Mobile-First Performance: What Makes React Native Apps Feel Native
After profiling six production React Native apps, I've found the same bottlenecks repeatedly. Here are the fixes that consistently move the needle.
The Threading Model You Must Understand
React Native has two critical threads [citation:1]:
| Thread | What It Runs | Frame Budget |
|---|---|---|
| JS Thread | Business logic, API calls, state updates | 16.67ms |
| UI/Main Thread | Scrolling, animations, native gestures | 16.67ms |
If either thread misses the budget, frames drop. Users notice immediately [citation:9].
List Performance: Replace FlatList with FlashList
FlatList's mount/unmount cycle kills scroll performance. Every scrolled item gets recreated from scratch, not recycled [citation:2].
// ❌ FlatList - mounts/unmounts items constantly
import { FlatList } from 'react-native';
<FlatList data={products} renderItem={renderProduct} />
// ✅ FlashList - recycles views, 10x faster
import { FlashList } from '@shopify/flash-list';
<FlashList
data={products}
renderItem={renderProduct}
estimatedItemSize={120} // Critical: average item height
getItemType={(item) => item.type} // For mixed types
/>
FlashList maintains a view pool — items stay mounted, data gets swapped . With React Native 0.82+ New Architecture, FlashList 2.0 leverages useLayoutEffect for even better precision .
Migration: npm install @shopify/flash-list → replace FlatList import → add estimatedItemSize. Done.
Navigation: Native Stack > JS Stack
Native stack navigators run transitions on the UI thread. JavaScript-based navigators block on the JS thread
// ✅ Native stack - animations on UI thread
import { createNativeStackNavigator } from '@react-navigation/native-stack';
// ❌ JS stack - blocks on JS thread
import { createStackNavigator } from '@react-navigation/stack';
For large apps, use nested navigators with lazy loading to reduce startup time
Offload Heavy Computation to Background Threads
Long-running JS blocks the thread → dropped frames. Use react-native-threadforge (C++ worker pool with Hermes VM isolation)
import { threadForge } from 'react-native-threadforge';
// Offload heavy calculation - UI stays at 60fps
const result = await threadForge.run(() => {
let total = 0;
for (let i = 0; i < 10e6; i++) total += Math.sqrt(i);
return total;
});
For animations, use react-native-reanimated worklets which run on the UI thread, not JS
The New Architecture Baseline
React Native 0.76+ enables New Architecture by default. Key performance wins
| Optimization | Impact |
|---|---|
| View flattening | Reduces native view count by merging non-visual parents |
| View recycling | Reuses views instead of destroy/recreate |
| Synchronous layout | useLayoutEffect works properly, powers FlashList 2.0 |
| React Transitions | Lower priority for non-urgent updates |
If a view disappears unexpectedly, set collapsable={false} to opt out of flattening .
The Optimization Checklist
-
Replace all FlatList with FlashList + estimatedItemSize
-
Use native stack navigator (native-stack over stack)
-
Enable Hermes (default in 0.70+, 0.82+ has Hermes V1 experimental)
-
Lazy-load screens with React.lazy + Suspense
-
Offload CPU work with threadForge
-
Remove console.log in production (use babel plugin)
-
Test in release mode — dev mode is 2-3x slower
The Bottom Line
The gap between "feels native" and "feels slow" is often one poor list component or a heavy sync calculation on the JS thread. FlashList solves the first. ThreadForge solves the second. Native stack navigator solves the third. These three changes alone have doubled frame rates in my production apps. The rest is fine-tuning