React Native Performance: What I Measure and Fix First
Source: Dev.to

What I Measure
-
Startup time – Time from tap to first meaningful paint (e.g., home screen visible).
Tools: Flipper, React Native’s built‑in perf tools, or a simple timestamp in native code. Compare before/after changes (new libs, more initial data) to avoid regressions. -
Frame rate (FPS) – Especially on scroll and animation.
Tools: React Native’s “Show Perf Monitor” or Flipper (shows JS and UI thread FPS). Drops below 60 FPS (or 120 FPS on capable devices) indicate jank. Note which screen or list causes it. -
List scroll – Long lists are a common bottleneck.
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?
- Are we using
-
Memory – Watch for leaks (memory growing over time) and large allocations.
Tools: Flipper or Xcode/Android Studio profilers. Leaks often stem from images, caches, or listeners not cleaned up. -
Bundle size – Large JS bundles slow startup.
Runnpx react-native bundle --dev falseand inspect the output size. Look for heavy dependencies that could be lazy‑loaded or replaced.
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 document or CI pipeline. When a new feature lands, compare against the baseline so regressions are obvious rather than “it feels slower.”
Fixes I Reach For First
Lists
// Example: enabling getItemLayout for fixed‑height items
const getItemLayout = (data, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
});
- Use
FlatList(orFlashListfor higher throughput). - Implement
getItemLayoutwhen item height is fixed. - Adjust
windowSizeandmaxToRenderPerBatchto limit items in the tree. - Memoize list item components to avoid re‑rendering on every scroll tick.
- Avoid inline functions and object literals in
renderItem; pass stable props.
Images
- Use
resizeModeappropriately. - Serve correctly sized images from the backend or a CDN that resizes.
- Consider
react-native-fast-imagefor better caching. - Lazy‑load off‑screen images in lists.
Re‑renders
const MemoizedComponent = React.memo(MyComponent);
const stableCallback = useCallback(() => { /* … */ }, []);
const stableValue = useMemo(() => computeExpensiveValue(), []);
- Detect unnecessary re‑renders with React DevTools Profiler or “Highlight updates.”
- Lift state so only the needed part re‑renders.
- 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 intensive computation, or batch work with
InteractionManager.runAfterInteractions. - Avoid large synchronous operations on the main JS thread during startup or scroll.
Startup
- Reduce initial JS bundle size: lazy‑load screens (
React.lazy+ code splitting where supported), defer non‑critical imports, and trim dependencies. - Enable Hermes if not already; it often improves startup time and memory usage.
Cleanup
- Unsubscribe from listeners, clear timers, and cancel requests in
useEffectcleanup. - Remove event listeners and close connections to prevent memory leaks or unnecessary background work.
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