| name | react-native-perf |
| description | Profile React Native with Hermes sampler, Flipper, and the New Architecture profiler. Use when JS thread, bridge, or layout commits are causing jank. |
React Native Performance
Instructions
React Native performance is split across three threads: JavaScript (Hermes), native UI, and the shadow/Fabric commit. Each has its own profiler and its own class of fixes.
1. Baseline Setup
- Use Hermes. It is the default in RN 0.70+ and required for sampling profiling. Confirm via
Platform.constants.reactNativeVersion and global.HermesInternal !== undefined.
- Use the New Architecture (Fabric + TurboModules). From RN 0.76 it is the default. Legacy Bridge is a perf liability.
- Test on release builds. Dev builds include warnings, source maps, and bridge overhead that skew numbers.
# Android
npx react-native run-android --mode=release
# iOS
npx react-native run-ios --configuration Release
2. Hermes Sampling Profiler
Record a CPU profile of the JS thread:
import * as Hermes from 'react-native/Libraries/SamplingProfiler';
Hermes.startSamplingProfiler();
Hermes.stopSamplingProfiler('/sdcard/hermes.cpuprofile');
Or from Flipper: Hermes Debugger → Profiler tab.
Pull and open in Chrome DevTools: chrome://inspect → DevTools → Performance → Load profile.
Read pattern:
- Top flame leaves are your hot JS functions.
- Wide
flushedQueue / callFunctionReturnFlushedQueue on the legacy bridge = migrate that call to a TurboModule.
- Wide
requireNativeComponent at startup = too many views instantiated up front.
3. Flipper Plugins
Install Flipper desktop. Useful plugins (2026 set):
| Plugin | What it shows |
|---|
| React DevTools | Component tree, props, render durations |
| Hermes Debugger | JS debugger + sampling profiler + heap snapshot |
| Network | Fetch / XHR requests with timing and bodies |
| Layout Inspector | Native view tree + bounds |
| Performance | JS FPS, UI FPS, RAM |
| Databases / AsyncStorage | Persisted state |
| Crash Reporter | Last crash stacks |
React DevTools Profiler: click Record, interact, stop. Each commit shows component render durations. Look for components rendering that shouldn't (use React.memo, useMemo, stable deps).
4. Common JS Thread Wins
-
Memoize rows in lists:
const Row = React.memo(({ item }: { item: Item }) => <Text>{item.title}</Text>);
-
Use stable callbacks:
const onPress = useCallback((id: string) => navigate(id), [navigate]);
-
Avoid creating new objects/arrays in render. Hoist constants.
-
Replace synchronous JSON.parse of large payloads with streaming / chunked parsing, or offload to a native module.
-
Use InteractionManager.runAfterInteractions for non-critical work after navigation.
5. Lists and Virtualization
FlatList is acceptable but has known gotchas. FlashList from Shopify is usually 2–5× faster.
import { FlashList } from '@shopify/flash-list';
<FlashList
data={items}
keyExtractor={item => item.id}
estimatedItemSize={96}
renderItem={({ item }) => <Row item={item} />}
/>
Always:
keyExtractor returns a stable id.
estimatedItemSize matches typical row height (within 10%).
removeClippedSubviews on Android (true) for long lists.
6. Animated vs Reanimated
- React Native Animated with
useNativeDriver: true is fine for transform/opacity animations.
- Reanimated 3 (worklets) runs on the UI thread — preferred for gesture-driven animations, list transitions, and anything synchronized with scroll.
const offset = useSharedValue(0);
const style = useAnimatedStyle(() => ({ transform: [{ translateY: offset.value }] }));
const gesture = Gesture.Pan().onUpdate(e => { offset.value = e.translationY; });
Avoid animating via setState at 60fps; every frame pays a JS → native hop.
7. Fabric Tracing and Commit Latency
Enable tracing in Flipper → Performance plugin → Fabric traces. Each commit shows:
- Shadow-tree diff duration.
- Mount-phase duration on the UI thread.
Commits > 8 ms at 120 Hz are trouble. Usually caused by:
- Very deep view trees (collapse wrapper
<View>s).
- Props churning (new inline style objects).
- Missing
View.collapsable in large groups.
8. Bridge and Native Modules
Legacy bridge calls are serialized JSON round-trips. Every call costs ~0.5–2 ms overhead. Migrate to TurboModules:
import type { TurboModule } from 'react-native/Libraries/TurboModule/RCTExport';
import { TurboModuleRegistry } from 'react-native';
export interface Spec extends TurboModule {
add(a: number, b: number): number;
}
export default TurboModuleRegistry.getEnforcing<Spec>('NativeMath');
Generated C++/Kotlin/Swift bindings skip the bridge.
9. Metro Bundle Size
npx react-native bundle --platform android --dev false --entry-file index.js --bundle-output bundle.js --sourcemap-output bundle.map
npx metro-bundle-visualizer bundle.map
Cut large lodash-like libs, remove unused icons from @expo/vector-icons.
10. Measure in Production
- Use Sentry Performance or Firebase Performance with RN SDKs.
- Instrument app startup with the
PerformanceObserver from react-native (Fabric exposes Performance.now).
- Track p95 JS FPS and commit duration per screen.
Checklist