How to Fix React Native Lag, Memory Leaks & Hit 60 FPS in Production
After auditing 12+ production React Native codebases, 80% of "the app feels slow" complaints trace back to the same 3 root causes.
Here's the exact fix for each.
1. Stop Re-Rendering Everything on Every State Tick
The most common frame drop: anonymous inline callbacks creating new function references on every parent render.
// ❌ New function on every render → child re-renders every time
<List onPress={() => navigate("Detail", { id })} />
// ✅ Stable reference → no unnecessary re-renders
const handlePress = useCallback(() => navigate("Detail", { id }), [id]);
<List onPress={handlePress} />
Also swap AsyncStorage for MMKV on mount reads — it's C++ JSI, synchronous, and 10x faster for cold-start hydration.
2. Memory Leaks — The Silent Killer
Apps using WebSockets, Supabase listeners, background GPS, or BLE leak memory every time a screen unmounts without cleaning up native subscriptions. Over 20 minutes: Android kills the process, iOS jetsams it.
useEffect(() => {
const controller = new AbortController();
const unsubscribe = socket.subscribe("channel", (payload) => {
if (!controller.signal.aborted) setState(payload);
});
return () => {
controller.abort();
unsubscribe(); // No zombie listeners, no RAM retention
};
}, [channelId]);
Rule: Every useEffect that opens a subscription, timer, or native listener must return a cleanup function. No exceptions.
3. FlatList → FlashList + Fix Your Images
FlatList doesn't recycle views during fast scrolling → blank cells → perceived lag.
// Drop-in replacement, same API
import { FlashList } from "@shopify/flash-list";
<FlashList
data={items}
renderItem={renderItem}
estimatedItemSize={72} // Measure your actual item height
/>
And never decode a 4000×3000px upload inside an 80×80px avatar:
// ✅ Request the right size from your CDN
const thumb = `${user.avatarUrl}?w=160&h=160&fit=crop`;
Use expo-image over the built-in Image for hardware-decoded caching.
4. Move Animations to the UI Thread
Any Animated.Value on the JS thread drops frames the moment a network call or navigation fires.
// ❌ JS thread — stutters during API calls
Animated.timing(opacity, { toValue: 1, useNativeDriver: true }).start();
// ✅ Reanimated worklet — C++ UI thread, frame-perfect
const opacity = useSharedValue(0);
const style = useAnimatedStyle(() => ({ opacity: withTiming(opacity.value) }));
Diagnostic Order
Profile on a real mid-range Android device in release mode — never Metro dev mode (timings are 3–5x slower).
1. React DevTools Profiler → find heaviest re-render tree
2. Audit all useEffects for missing cleanup
3. FlatList → FlashList on any list > 50 items
4. CDN-resize images to display size
5. Migrate animations to Reanimated worklets
6. Enable New Architecture in app.json (Expo SDK 51+)
Performance bugs in React Native are diagnosable, not mysterious. These 4 patterns cover 90% of real-world production cases.
If your existing app is lagging or crashing on Android, I write about more patterns like this at innunext.com and do deep-dive production audits.
Tags: reactnative javascript mobile performance typescript
Top comments (0)