When a React Native app feels slow, it’s easy to guess. Here’s what I measure first and the fixes I reach for most often.
What I Measure
Startup time — Time from tap to first meaningful paint (e.g. home screen visible). I use Flipper, React Native’s built-in perf tools, or a simple timestamp in native code. I compare before/after changes (e.g. new libs, more initial data) so we don’t regress.
Frame rate (FPS) — Especially on scroll and animation. React Native’s “Show Perf Monitor” or Flipper shows JS and UI thread FPS. Drops below 60 (or 120 on capable devices) mean jank. I note which screen or list causes it.
List scroll — Long lists are a common bottleneck. I check: Are we using FlatList (or similar)? Are we rendering too many items or heavy components per row? Are we doing work on the JS thread during scroll?
Memory — I watch for leaks (memory growing over time) and big allocations. Flipper or Xcode/Android Studio profilers show what’s growing. Often it’s images, caches, or listeners not cleaned up.
Bundle size — Large JS bundles slow startup. I check the output of the build (e.g. npx react-native bundle --dev false and inspect size). I look for heavy dependencies that could be lazy-loaded or replaced.
I don’t optimize everything—I focus on startup and the screens users hit most (e.g. home, main list). Fix the biggest wins first.
Baseline per release — Track a small set of numbers per release (e.g. startup time, list FPS on the main feed, memory after 5 minutes). Store them in a doc or CI. When a new feature lands, compare against the baseline so regressions are obvious instead of “it feels slower.”
Fixes I Reach For First
Lists — Use FlatList (or FlashList if we need more throughput). Implement getItemLayout when item height is fixed so we skip measurement. Use windowSize and maxToRenderPerBatch to limit how many items are in the tree. Memoize list item components so they don’t re-render on every scroll tick. Avoid inline functions and object literals in renderItem; pass stable props.
Images — Use resizeMode appropriately. Serve correctly sized images from the backend or use a CDN that resizes. Consider react-native-fast-image for caching if the default cache isn’t enough. Lazy-load off-screen images in lists.
Re-renders — Find unnecessary re-renders with React DevTools Profiler or “Highlight updates.” Often the fix is: memoize components (React.memo), stabilize callbacks (useCallback) and objects/arrays (useMemo), or lift state so only the part that needs to re-render does. Avoid setting state in render or in effects that run too often.
JS thread — Move heavy work off the JS thread: use native modules for heavy computation, or do work in batches (e.g. InteractionManager.runAfterInteractions). Don’t do large sync operations on the main JS thread during startup or scroll.
Startup — Reduce initial JS: lazy-load screens (e.g. React.lazy + code splitting where supported), defer non-critical imports, and trim dependencies. Use Hermes if you’re not already; it often improves startup and memory.
Cleanup — Unsubscribe from listeners, clear timers, and cancel requests in useEffect cleanup. Remove event listeners and close connections so we don’t leak memory or keep unnecessary work running.
I fix one area at a time, measure again, and repeat. Small, verified improvements add up; random “optimizations” without measurement often don’t.
Saad Mehmood — Portfolio
Top comments (2)
Great breakdown. I like that you focus on measurement before optimization, especially startup + main list first.
One thing I’d add: track a small perf baseline per release (startup time, list FPS, memory after 5 minutes). It makes regressions obvious when new features land.
Curious about your experience with FlashList vs tuned FlatList on mid-range Android devices, did you see a consistent FPS gain?
Thanks, glad the measurement-first approach resonated.
On FlashList vs. tuned FlatList on mid-range Android: I’ve seen a consistent FPS gain with FlashList when the list is long and/or list items are moderately heavy (images, several components per row). On mid-range devices, FlashList’s recycling and layout work often keeps scroll closer to 60 FPS than a well-tuned FlatList with getItemLayout and memoization. When items are very simple (plain text, no images), the difference is smaller, and a tuned FlatList can be close. So I’d say: if the main feed or a critical list is already a bottleneck on mid-range Android, try FlashList and measure; if it’s already smooth with FlatList, the gain may not be worth the extra dependency. I still measure before and after when switching.