Meta description: Aprenda a debugar memory leaks no React Native FlashList com Hermes Profiler e Flipper — economizei 200MB+ de RAM em produção seguindo esses passos.
Last updated: May 27, 2025
I was three weeks from shipping a social feed feature when my QA lead sent me a Slack message: “The app crashes after scrolling for about two minutes.” My first instinct was to blame the API. It wasn’t the API. It was a classic memory leak in React Native FlashList — and it took me two days to fully understand why it was happening. If you’re building high-performance list screens with Shopify’s FlashList and noticing growing memory usage, crashes, or sluggish scroll behavior, this guide is exactly what you need.
TL;DR
- FlashList recycles item components — closures and event listeners that aren’t cleaned up in
useEffectwill persist across recycled cells. - Use the Hermes Profiler + Flipper (or the new React Native DevTools) to catch memory growth before it becomes a crash.
- The three most common culprits are: uncleared intervals/timeouts, orphaned event emitter subscriptions, and image cache bloat.
Why Memory Leaks in FlashList Are Different From FlatList
FlashList, built by Shopify, achieves its performance gains through cell recycling — it reuses rendered component instances instead of unmounting and remounting them. This is fundamentally different from how FlatList works, and it changes the memory leak surface entirely.
With FlatList, when a cell scrolls off screen it unmounts. Cleanup happens naturally via useEffect return functions. With FlashList, the cell doesn’t unmount — it gets recycled and reused for a new item. If your component registers a listener, sets up a timer, or captures a closure over stale data, that baggage accumulates across every recycled render.
Important: FlashList’s recycling is the feature, not the bug. The bug is code that assumes unmount-based cleanup — you must actively write recycling-safe components.
Prerequisites
Before diving in, make sure you have:
- React Native
0.71+ @shopify/flash-list1.6.xor later- Flipper
0.201+or React Native DevTools (bundled with RN0.73+) - Hermes engine enabled (default since RN
0.70) - Node.js
18+and your project running on a physical device or a simulator with at least 4GB RAM
Step-by-Step: How to Debug Memory Leaks in React Native FlashList
Step 1 — Reproduce the Leak Consistently
Before you can fix anything, you need a reproducible scenario. I wrote a simple stress test: scroll the list to the bottom and back to the top, five times, then take a memory snapshot.
# Check current memory usage via adb (Android)
adb shell dumpsys meminfo com.yourapp.package | grep "TOTAL"
On iOS, use Xcode’s Memory Graph Debugger (Debug → Memory Graph) or the Instruments Allocations template. Seeing TOTAL RSS grow by more than ~20MB per full scroll cycle is a red flag.
Step 2 — Enable the Hermes Profiler
Hermes ships with a sampling profiler. Enable it in your metro.config.js and connect via Flipper’s Hermes Debugger plugin.
// metro.config.js
const { getDefaultConfig } = require('@react-native/metro-config');
const config = getDefaultConfig(__dirname);
config.transformer.hermesCommand = './node_modules/react-native/sdks/hermesc/osx-bin/hermesc';
module.exports = config;
Once connected in Flipper, go to Hermes Debugger → Memory → Start before scrolling, then Stop after your stress test. You’re looking for objects in the Detached state — these are the leaked ones.
Step 3 — Identify the Leak Source
In my app, I found this pattern in a VideoCard component:
// ❌ BAD — leaks on every recycle
useEffect(() => {
const subscription = EventEmitter.addListener('playbackUpdate', handleUpdate);
// No cleanup!
}, []);
Because FlashList recycles the component without unmounting it, the useEffect with an empty dependency array only runs once — on the very first mount. The subscription from the previous item’s data is never removed. After 50 recycles, you have 50 dangling listeners.
Step 4 — Fix Event Listener Leaks
Always return a cleanup function, and — critically — re-subscribe when the item data changes:
// ✅ GOOD — cleanup tied to itemId
useEffect(() => {
const subscription = EventEmitter.addListener('playbackUpdate', handleUpdate);
return () => {
subscription.remove();
};
}, [itemId]); // Re-run when item changes due to recycling
The key change is the dependency array. Adding itemId (or whatever uniquely identifies your data row) ensures the effect re-runs when FlashList recycles the cell with new data — properly cleaning up the old subscription first.
Step 5 — Fix Timer and Interval Leaks
I also had countdown timers in auction cards:
// ❌ BAD — interval keeps firing for old item data
useEffect(() => {
const id = setInterval(() => {
setTimeLeft(calculateTimeLeft(item.endsAt));
}, 1000);
}, []);
// ✅ GOOD
useEffect(() => {
const id = setInterval(() => {
setTimeLeft(calculateTimeLeft(item.endsAt));
}, 1000);
return () => clearInterval(id);
}, [item.endsAt]);
Step 6 — Fix Image Cache Bloat
React Native’s default Image component caches aggressively. In a FlashList with hundreds of items, this can grow to 300–500MB on low-end Android devices. I switched to expo-image (or react-native-fast-image) with explicit cache policy settings:
import { Image } from 'expo-image';
<Image
source={{ uri: item.thumbnailUrl }}
cachePolicy="memory-disk"
recyclingKey={item.id} // 🔑 Critical for FlashList
style={styles.thumbnail}
/>
The recyclingKey prop is non-obvious but essential — it tells the image component to treat each recycled cell as a fresh render, preventing stale images from flashing before the new one loads.
Step 7 — Validate With a Second Profiler Pass
After your fixes, re-run the stress test and take a new Hermes memory snapshot. Compare object counts. In my case, EventEmitterListener objects dropped from 847 → 12 after the cleanup fix. Total memory after 5 scroll cycles went from 340MB down to 118MB.
Real-World Tips I Use in Production
Use overrideItemLayout to prevent layout thrashing. If you don’t provide accurate item sizes, FlashList will measure each cell and re-layout — this creates temporary allocations that never get GC’d cleanly.
<FlashList
overrideItemLayout={(layout, item) => {
layout.size = item.type === 'video' ? 280 : 120;
}}
/>
Set drawDistance conservatively. The default is 250. I dropped mine to 150 on lower-end Android targets and saw 30% less pre-render memory usage.
Memoize list items aggressively. Wrap item components in React.memo with a custom comparison function. Unnecessary re-renders inside recycled cells cause ghost allocations.
Common Errors and How I Fixed Them
Error: Warning: Can't perform a React state update on an unmounted component This still fires in FlashList even though the component isn’t truly unmounted — it fires when the isFocused context changes. Fix: use a useIsMounted ref hook to guard all async state updates.
Error: App crashes with JSIError: Cannot read property 'id' of undefined This happened when a recycled cell’s useEffect ran with stale closure data from the previous item before the new item prop was available. Fix: add item.id to the dependency array of every effect that references item.
Error: Memory not releasing after navigating away from the list screen Root cause was a useRef holding a reference to the FlashList instance and being stored in a module-level variable (outside the component). Fix: always store refs inside the component or clean them up in the screen’s useEffect unmount.
FAQ
Why does React Native FlashList use more memory than FlatList in some cases?
FlashList keeps a pool of recycled components in memory at all times to enable instant reuse. If those components hold uncleared subscriptions or closures, the pool itself becomes a leak source. FlatList unmounts cells, so leaks are self-healing on scroll — FlashList’s leaks are permanent until the screen unmounts.
How do I detect memory leaks in React Native FlashList on iOS?
Use Xcode Instruments with the “Leaks” or “Allocations” template. Run your scroll stress test and watch for growing allocation counts in your custom component classes. You can also use the React DevTools Profiler (Flamegraph view) to spot components that re-render unexpectedly on each scroll.
Does using keyExtractor in FlashList prevent memory leaks?
keyExtractor helps FlashList decide which items map to which recycled cells, which can reduce stale closure bugs. It doesn’t prevent leaks by itself, but incorrect or missing keyExtractor implementations can make leaks worse by causing unnecessary re-subscriptions.
What is the best image library to avoid memory leaks with FlashList?
In my experience, expo-image with recyclingKey set to the item’s unique ID is the most reliable option. It was built with FlashList’s recycling model in mind. react-native-fast-image is also solid but requires manual cache management for large lists.
How do I profile memory usage in a React Native app without Flipper?
Since RN 0.73, the bundled React Native DevTools (accessible via j in the Metro terminal) includes a basic memory view. For production builds, use adb shell dumpsys meminfo on Android or Xcode’s Memory Report panel on iOS. Firebase Performance Monitoring can also surface crash-correlated memory spikes in production.
Conclusion
Memory leaks in React Native FlashList are sneaky precisely because the library’s best feature — cell recycling — is the root cause. Once I understood that my components needed to be written as recycling-aware, not just unmount-aware, the fixes were straightforward. The payoff was real: our feed screen went from crashing every two minutes to running stable for hours of continuous use.
About the Author
I’m a senior mobile engineer with over 8 years of experience building React Native applications for fintech and e-commerce companies. My day-to-day stack is React Native, TypeScript, Node.js, and AWS — and I’ve shipped apps to over 2 million users. I write about mobile performance, architecture patterns, and the debugging war stories nobody else posts about.

