the legacy bridge entirely, enabling direct C++/JS memory sharing and synchronous UI thread updates.
Core Solution
Optimization requires a phased, architecture-aware approach. Implement each layer sequentially; skipping steps introduces regressions.
Phase 1: Threading & Execution Model Mastery
React Native runs three primary threads: JS, UI (main), and Native Modules. The JS thread handles logic, state updates, and bridge communication. The UI thread renders views and handles gestures. Blocking either causes jank.
Implementation:
- Offload heavy computation to
react-native-reanimated worklets or native modules.
- Never run synchronous JSON parsing, image decoding, or cryptographic operations on the JS thread.
- Use
InteractionManager.runAfterInteractions() for non-critical updates during gestures.
// ❌ Blocks JS thread during scroll
const parseData = (raw) => JSON.parse(raw);
// ✅ Offloaded to Web Worker / Native Module
import { runOnJS, useWorklet } from 'react-native-reanimated';
const parseWorklet = useWorklet((raw) => {
'worklet';
const result = JSON.parse(raw);
runOnJS(updateState)(result);
});
Phase 2: Rendering Pipeline Optimization
Re-render explosions are the #1 cause of frame drops. React’s reconciliation engine diffes the entire tree unless explicitly constrained.
Implementation:
- Replace
FlatList with FlashList (Shopify). It uses recycled views and measures items dynamically, eliminating off-screen allocation.
- Wrap list items in
React.memo and stabilize prop references.
- Use
useMemo for derived state and useCallback for event handlers passed to children.
import { FlashList } from '@shopify/flash-list';
const MemoizedFeedItem = React.memo(({ item, onPress }) => {
return (
<TouchableOpacity onPress={onPress}>
<FastImage source={{ uri: item.thumbnail }} style={styles.image} />
<Text numberOfLines={2}>{item.title}</Text>
</TouchableOpacity>
);
});
// In parent
const handlePress = useCallback((id) => { /* ... */ }, []);
return <FlashList data={items} renderItem={({ item }) => <MemoizedFeedItem item={item} onPress={() => handlePress(item.id)} />} estimatedItemSize={120} />;
Phase 3: State Management & Re-Render Control
Context API triggers full subtree re-renders on any value change. For high-frequency updates (scroll position, WebSocket streams, form inputs), external stores with selector-based subscriptions are mandatory.
Architecture Decision: Use Zustand or Redux Toolkit with useSelector and shallow equality. Avoid useContext for state that changes >60Hz.
import { create } from 'zustand';
const useFeedStore = create((set) => ({
items: [],
loading: false,
appendItems: (newItems) => set((state) => ({
items: [...state.items, ...newItems],
})),
}));
// Component subscribes only to specific slice
const items = useFeedStore((state) => state.items);
Phase 4: Native Bridge & Fabric Migration
The legacy bridge serializes all JS↔Native calls. Fabric replaces it with a synchronous C++ renderer and TurboModules enable direct method invocation.
Implementation:
- Enable
bridgeless mode in AppDelegate.mm and MainApplication.java.
- Migrate critical native modules to TurboModules using the New Architecture codegen.
- Use
react-native-mmkv or react-native-sqlite-storage for disk I/O instead of AsyncStorage.
// Android: MainApplication.java
@Override
protected boolean isBridgelessEnabled() {
return true;
}
// iOS: AppDelegate.mm
- (BOOL)bridgelessEnabled {
return YES;
}
Profiling Workflow
Performance optimization without measurement is guesswork. Establish a profiling pipeline:
- JS Thread: React DevTools Profiler → record interactions → identify wasted renders.
- Native Thread: Android Studio Profiler / Xcode Instruments → trace UI thread block time.
- Bridge/Architecture:
systrace or Perfetto → measure frame composition time and GC pauses.
- Memory: Flipper Memory Plugin → snapshot heap → detect retained references and image leaks.
Run baseline profiles before and after every optimization. Track FPS stability, JS block time, and memory delta.
Pitfall Guide
-
Treating useEffect as a render trigger
useEffect runs after paint. Using it for data fetching or state derivation causes double renders and layout thrashing. Prefer useMemo for synchronous derivation and React Query/SWR for async data.
-
Rendering full-resolution images in lists
Loading 4K images into memory for 100px thumbnails spikes heap usage and triggers GC pauses. Always use react-native-fast-image with explicit resizeMode, cacheControl, and CDN-resized URLs.
-
Ignoring Hermes bytecode compilation
Hermes compiles JS to bytecode at build time, eliminating V8 startup overhead and reducing memory fragmentation. Disabling it for debugging in production builds increases cold start by 30–40%.
-
Synchronous bridge calls blocking UI
Passing large arrays, base64 strings, or unfiltered objects across the bridge freezes the JS thread. Serialize to JSON, chunk payloads, or move logic to native modules.
-
Missing keyExtractor or using array index as key
Index keys break reconciliation when items reorder or filter. React re-renders entire lists unnecessarily. Always use stable, unique identifiers.
-
Over-engineering with Context for high-frequency updates
Context triggers O(n) re-renders. For scroll offsets, keyboard state, or real-time streams, use external stores or useRef + custom event emitters.
-
Skipping memory profiling for async tasks
Unresolved promises, lingering event listeners, and uncleared intervals retain references. Use useRef for timers, cleanup in useEffect, and snapshot memory before/after navigation.
Production Bundle
Action Checklist
Decision Matrix
| Strategy | Use When | Avoid When | Performance Impact |
|---|
| Hermes vs JSC | Production builds, memory-constrained devices | Debugging native C++ crashes | ↓ Cold start 30%, ↓ GC pauses 40% |
| FlashList vs FlatList | Lists >50 items, dynamic heights, media | Static <20 item lists | ↓ Memory 60%, ↑ FPS stability |
| Context vs External Store | Low-frequency app config/theme | Scroll, form, WebSocket data | ↓ Re-renders 70% with selectors |
| Bridge vs Bridgeless | Legacy native modules, quick POC | Production, gesture-heavy apps | ↓ Latency 80%, ↑ UI thread responsiveness |
| AsyncStorage vs MMKV | Simple key-value, offline config | High-frequency state, large payloads | ↓ Read/Write latency 90% |
Configuration Template
metro.config.js
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
const defaultConfig = getDefaultConfig(__dirname);
const config = {
transformer: {
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: true,
},
}),
},
resolver: {
sourceExts: [...defaultConfig.resolver.sourceExts, 'mjs'],
},
};
module.exports = mergeConfig(defaultConfig, config);
android/app/build.gradle (Hermes + ProGuard)
project.ext.react = [
enableHermes: true,
hermesFlagsRelease: ["-O", "-output-source-map"],
]
def enableProguardInReleaseBuilds = true
android {
buildTypes {
release {
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
}
}
}
babel.config.js (Production Optimizations)
module.exports = {
presets: ['module:@react-native/babel-preset'],
plugins: [
['react-native-reanimated/plugin', { relativeSourceLocation: true }],
'react-native-paper/babel',
],
env: {
production: {
plugins: [
'react-native-paper/babel',
['@babel/plugin-transform-runtime', { regenerator: true }],
],
},
},
};
Quick Start Guide
-
Baseline Profile
Run systrace or React DevTools Profiler on your current app. Record FPS, JS block time, and memory usage for your heaviest screen.
-
Enable Hermes & Metro Optimizations
Set enableHermes: true, enable inlineRequires, and run a release build. Verify bytecode compilation with hermes --version.
-
Swap List Implementation
Replace FlatList with FlashList. Provide estimatedItemSize and wrap items in React.memo. Remove index-based keys.
-
Stabilize State & Handlers
Migrate high-frequency state to an external store. Wrap event handlers in useCallback. Profile again and compare deltas.
-
Iterate with Native Modules
Identify JS thread bottlenecks via systrace. Migrate image decoding, parsing, or crypto to react-native-reanimated worklets or TurboModules. Re-measure.
Performance in React Native is not a framework limitation; it is an architecture discipline. By enforcing threading boundaries, constraining re-renders, adopting bridgeless execution, and profiling continuously, teams consistently achieve stable 60 FPS, sub-700ms cold starts, and predictable memory footprints across mid-tier and flagship devices.