
React Native Performance in 2025: A Field Guide
React Native Performance in 2025: A Field Guide
Practical tactics to make your React Native apps feel instant: faster startup, smoother rendering, crisp animations, and lean memory—without breaking developer velocity.
Who this is for
- Teams on RN 0.7x+ (Hermes enabled)
- Apps adopting the New Architecture (Fabric/TurboModules)
- Product squads owning perf KPIs (TTI, INP, cold start)
What you’ll improve
- Cold & warm startup, bundle size
- List scrolling & interaction latency
- Animation smoothness & memory usage
How to use this guide
Start with the Quick Checklist, then apply targeted fixes per section. Measure before/after—ship improvements weekly.
Quick checklist (ship these first)
- Enable Hermes in release (and debug if feasible). Verify with
global.HermesInternal. - Adopt the New Architecture (Fabric/TurboModules) where your dependencies support it.
- Split JS work: move heavy
require()/initialization behindimport()and lazy screens. - Virtualize lists properly:
FlatListwith sanewindowSize,getItemLayout, stable keys, and memoized rows. - Run animations off the JS thread (Reanimated worklets or native driver where appropriate).
- Optimize images: correct sizes/aspect ratios, prefetch, caching component, and defer decoding off critical paths.
- Trim the bundle: remove unused modules, tree-shake, and code-split rarely used screens.
- Measure with Flipper/Instruments/Perfetto; track cold start, TTI, and interaction latency in production.
1) Measure what matters
- Startup: cold/warm time, JS bundle eval, first interactive screen.
- Interaction latency: tap-to-response, gesture latency, input delay.
- Rendering: dropped frames during scroll, animation jitter.
- Memory: steady-state usage, leaks on navigation loops.
Tools: Flipper (React DevTools & Perf), Android Perfetto/Systrace, iOS Instruments (Time Profiler, Allocations).
2) Faster startup
- Hermes: ensure Bytecode is shipped; disable dev features in release.
- Defer heavy work: move non-critical requires to
import(); initialize SDKs after first render viaInteractionManager.runAfterInteractions. - Lazy screens: split navigation stacks and lazy-load deep screens.
- Splash discipline: lightweight native splash; avoid JS work before first paint.
- Reduce JS surface: delete dead modules, avoid polyfills you don’t need.
// Defer non-critical modules
useEffect(() => {
let mounted = true;
InteractionManager.runAfterInteractions(async () => {
if (!mounted) return;
const { initAnalytics } = await import("./analytics");
initAnalytics();
});
return () => { mounted = false; };
}, []);
3) Rendering: fewer, cheaper re-renders
- Use
React.memofor stable, pure row/components. - Wrap callbacks in
useCallbackand derive props withuseMemo. - Lift state up; avoid passing new object/array literals each render.
- Normalize store selectors to avoid thrashy updates.
// Stable row with memo + keyExtractor
const Row = React.memo(({ item }) => {
return (
{item.title}
);
});
it.id}
renderItem={({ item }) =>
}
/>
4) Lists: virtualization that feels native
Configure FlatList for your content physiology—don’t ship defaults blindly.
it.id}
renderItem={renderRow}
initialNumToRender={10}
windowSize={7}
maxToRenderPerBatch={10}
updateCellsBatchingPeriod={50}
removeClippedSubviews
getItemLayout={(data, index) => (
{ length: ROW_HEIGHT, offset: ROW_HEIGHT * index, index }
)}
/>
- Provide
getItemLayoutfor fixed-height rows. - Use
removeClippedSubviewson long feeds. - Avoid nested
ScrollViews; prefer one virtualized list.
5) Animations: keep them off the JS thread
Use a native/UISide execution path (e.g., Reanimated worklets) for buttery 60fps when JS is busy.
// Example with Reanimated (pseudo)
const progress = useSharedValue(0);
useEffect(() => { progress.value = withTiming(1, { duration: 400 }); }, []);
const style = useAnimatedStyle(() => ({
transform: [{ translateY: (1 - progress.value) * 20 }],
opacity: progress.value,
}));
return ;
- Prefer transform/opacity over expensive layout properties.
- Coalesce animations; avoid overlapping layout thrash.
6) Images: right size, right time
- Serve appropriately sized assets; set explicit width/height to avoid layout shifts.
- Prefetch hero images during splash or previous screen transitions.
- Use an image component with caching; avoid re-decoding on scroll.
// Prefetch before showing a gallery
useEffect(() => {
Image.prefetch(url);
}, [url]);
7) Memory: avoid slow leaks
- Unsubscribe timers, listeners, and event buses on unmount.
- Recycle large images; prefer thumbnails in feeds.
- Beware of retaining closures with big captured objects.
- Test navigation loops for growing memory—profile with Instruments/Perfetto.
8) Bridge & JSI: fewer crossings, bigger batches
- Batch interactions; avoid chatty per-frame bridge calls.
- Use JSI/TurboModules for high-frequency ops where available.
- Defer non-UI work to background threads in native modules.
// Pseudo: debounce chatty updates before crossing the boundary
const batchedSend = debounce((payloads) => NativeModule.send(payloads), 16);
9) Release build hygiene
Android
minifyEnabled true,shrinkResources true; keep rules for RN/JSI libs.- Enable Hermes; verify no dev flags in release.
- Use
android:hardwareAccelerated="true"(default) for animation-heavy screens. - Profile with Perfetto; check startup slices and binder contention.
iOS
- Release scheme with dead code stripping; optimize for size/speed per needs.
- Avoid large asset catalogs in the main app target—lazy-load packs when possible.
- Profile with Instruments (Time Profiler, Allocations, Core Animation).
Common pitfalls
- Rendering huge lists inside
ScrollView(no virtualization). - Anonymous inline components/functions causing re-renders.
- Large synchronous
require()chains at app start. - Animations running on the JS thread during heavy work.
- Image components without caching in media-heavy feeds.
Do I need the New Architecture to be fast?
Not strictly—but Fabric/TurboModules can reduce overhead for rendering/native calls. Adopt where your dependencies are compatible; measure before/after.
Should I swap libraries for performance?
Only when profiling proves a bottleneck. Often configuration (lists, images, animations) unlocks most wins without churn.