Welcome to Day 19 of the React Native Advent Calendar!
Today we’re diving into 7 essential performance tips that will make your React Native apps faster, smoother, and more responsive. These are battle-tested strategies from real-world apps!
Todays tips are taken from my article React Native performance tactics: Modern strategies and tools and condensed into 7 short tips.
TIP 1: Optimize App Startup
The problem: Slow Time to Interactive (TTI) makes users think your app is broken.
The fix:
Use Code Splitting with Expo Router
Enable async routes to split your bundle:
{
"experiments": {
"asyncRoutes": true
}
}Analyze Your Bundle
Find what’s bloating your startup:
# Visualize bundle size with Expo Atlas
EXPO_ATLAS=true npx expo startProfile Startup Performance
# Open DevTools and press 'j' to open the Profiler
npx expo start
# Press 'j' → Profiler tab → Record startupQuick wins:
- Defer non-critical imports
- Lazy load heavy screens
- Move expensive computations to after first render
TIP 2: Stop Unnecessary Renders
The problem: Components re-rendering when nothing changed = wasted CPU cycles.
Enable React Compiler (Expo SDK 54+)
Automatic memoization without useMemo or React.memo:
{
"experiments": {
"reactCompiler": true
}
}Visualize Re-renders
# In React DevTools:
# 1. Open Settings (gear icon)
# 2. Enable "Highlight updates when components render"
# 3. Watch your app - flashing boxes = re-rendersManual Optimization (Pre-Compiler)
import { memo } from 'react';
// Only re-renders if props.user changes
export const UserCard = memo(({ user }) => {
return (
<View>
<Text>{user.name}</Text>
</View>
);
});TIP 3: Fix Animation Jank
The problem: Animations stutter because they run on the JS thread.
The fix: Use Reanimated to run animations on the UI thread.
Before (Janky)
// ❌ Runs on JS thread - stutters during heavy work
const [position] = useState(new Animated.Value(0));
Animated.timing(position, {
toValue: 100,
duration: 300,
useNativeDriver: true
}).start();After (Smooth)
// ✅ Runs on UI thread - always 60fps
import { useSharedValue, withTiming } from 'react-native-reanimated';
const position = useSharedValue(0);
// Runs on UI thread even during heavy JS work
position.value = withTiming(100, { duration: 300 });Monitor FPS in Real-Time
# Enable Performance Monitor
npx expo start
# Press 'd' → Show Performance Monitor
# Watch FPS while testing animationsTIP 4: Lists That Don’t Suck
The problem: Rendering 1000 items at once = instant crash.
The fix: Use FlashList (or optimized FlatList).
Use FlashList
npx expo install @shopify/flash-listimport { FlashList } from '@shopify/flash-list';
export function UserList({ users }) {
return (
<FlashList
data={users}
renderItem={({ item }) => <UserCard user={item} />}
estimatedItemSize={80}
// That's it! FlashList handles optimization
/>
);
}Optimize FlatList (if you can’t use FlashList)
<FlatList
data={users}
renderItem={({ item }) => <UserCard user={item} />}
keyExtractor={(item) => item.id} // ✅ Stable keys
removeClippedSubviews={true} // ✅ Unmount off-screen items
maxToRenderPerBatch={10} // ✅ Render in batches
windowSize={5} // ✅ Keep 5 screens worth of items
getItemLayout={(data, index) => ({
// ✅ Skip layout calculation if items have fixed height
length: 80,
offset: 80 * index,
index
})}
/>TIP 5: Avoid Global State Overkill
The problem: Every state change triggers ALL subscribers to re-render.
The fix: Use granular state and selectors.
Bad: Global State Triggers Everything
// ❌ Changing theme re-renders EVERY component using this store
export const useStore = create((set) => ({
user: { name: 'John' },
theme: 'light',
cart: [],
updateTheme: (theme) => set({ theme })
}));Good: Select Only What You Need
// ✅ Only re-renders when theme changes
export function Header() {
const theme = useStore((state) => state.theme);
return <View style={{ backgroundColor: theme === 'dark' ? '#000' : '#fff' }} />;
}Better: Split Stores
// ✅ Separate stores for separate concerns
export const useThemeStore = create((set) => ({
theme: 'light',
toggleTheme: () => set((s) => ({ theme: s.theme === 'light' ? 'dark' : 'light' }))
}));
export const useUserStore = create((set) => ({
user: null,
setUser: (user) => set({ user })
}));TIP 6: Fix Memory Leaks
The problem: Timers, listeners, and subscriptions that never clean up = app crashes.
The fix: Always clean up in useEffect.
Common Memory Leaks
// ❌ Memory leak - interval never clears
useEffect(() => {
const interval = setInterval(() => {
console.log('Tick');
}, 1000);
// Missing cleanup!
}, []);
// ✅ Proper cleanup
useEffect(() => {
const interval = setInterval(() => {
console.log('Tick');
}, 1000);
return () => clearInterval(interval);
}, []);Abort Fetch Requests
useEffect(() => {
const controller = new AbortController();
fetch('https://api.example.com/users', {
signal: controller.signal
})
.then((res) => res.json())
.then(setUsers)
.catch((err) => {
if (err.name !== 'AbortError') console.error(err);
});
return () => controller.abort(); // ✅ Cancel on unmount
}, []);Clean Up Event Listeners
useEffect(() => {
const subscription = Keyboard.addListener('keyboardDidShow', handleShow);
return () => subscription.remove(); // ✅ Remove listener
}, []);TIP 7: Measure Performance
The problem: You can’t improve what you don’t measure.
The fix: Use tools to track performance in development AND production.
Development Tools
React DevTools Profiler:
# Press 'j' in Expo Dev Tools → Profiler
# Record interaction → See flamegraph of rendersPerformance Monitor:
# Press 'd' → Show Performance Monitor
# Watch: JS FPS, UI FPS, RAM usageQuick Performance Checklist
Before shipping your app, verify:
- ✅ TTI below 3 seconds
- ✅ Animations run at 60fps
- ✅ Lists scroll smoothly with 1000+ items
- ✅ No memory leaks (test by navigating between screens 20+ times)
- ✅ Bundle size optimized (check with Expo Atlas)
- ✅ Production monitoring enabled (Sentry or similar)
- ✅ Performance tested on low-end devices
Wrapping Up
Performance optimization isn’t a one-time task - it’s an ongoing process. These 7 tips cover the most common bottlenecks in React Native apps.
Quick recap:
- Optimize startup - Code split, defer imports, analyze bundle
- Stop unnecessary renders - Use React Compiler or manual memoization
- Fix animation jank - Use Reanimated for 60fps animations
- Lists that don’t suck - FlashList or optimized FlatList
- Avoid state overkill - Use selectors and split stores
- Fix memory leaks - Always clean up in useEffect
- Measure performance - DevTools + production monitoring
Ready to dive deeper?
Improved your app’s performance? Share your wins on Twitter!
Tomorrow we’ll explore Day 20’s topic - see you then! 🎄