React Native apps can feel buttery smooth or frustratingly janky — and the difference often comes down to a handful of well-known pitfalls. After shipping a dozen production apps, here are the eight optimisations I apply every time.
1. Use the New Architecture (Fabric + JSI)
If you're on React Native 0.71+, enable the New Architecture. The old bridge was the root cause of most performance ceilings.
// android/gradle.properties
newArchEnabled=true
# ios/Podfile
ENV['RCT_NEW_ARCH_ENABLED'] = '1'
The New Architecture eliminates the async bridge entirely, enabling synchronous calls between JS and native.
2. Stop Re-rendering Everything
The most common cause of jank is unnecessary re-renders. Use React.memo, useMemo, and useCallback strategically:
// Bad — new function reference on every render
<FlatList
data={items}
renderItem={({ item }) => <ItemCard item={item} />}
/>
// Good — stable reference
const renderItem = useCallback(
({ item }: { item: Item }) => <ItemCard item={item} />,
[] // no deps — ItemCard handles its own data
);
<FlatList data={items} renderItem={renderItem} />
3. FlatList Configuration
FlatList is your most performance-critical component. These props matter:
<FlatList
data={items}
renderItem={renderItem}
keyExtractor={(item) => item.id}
// Reduces initial render cost
initialNumToRender={10}
// How far ahead to render
windowSize={5}
// Remove off-screen components from memory
removeClippedSubviews={true}
// Batch state updates
updateCellsBatchingPeriod={50}
maxToRenderPerBatch={10}
/>
4. Avoid Inline Styles
Inline style objects are recreated on every render. Use StyleSheet.create:
// Bad
<View style={{ flex: 1, backgroundColor: "#fff", padding: 16 }}>
// Good
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: "#fff", padding: 16 },
});
<View style={styles.container}>
5. Use Hermes
Hermes is a JS engine optimised for React Native. Enable it:
// android/app/build.gradle
project.ext.react = [
enableHermes: true,
]
Hermes pre-compiles JS to bytecode at build time, significantly reducing startup time (often 20–40% faster TTI).
6. Image Optimisation
Images are a major culprit for memory issues. Always:
// Use explicit dimensions — avoids layout thrashing
<Image
source={{ uri: imageUrl }}
style={{ width: 200, height: 200 }}
// Cache on disk
resizeMode="cover"
/>
For production apps, use expo-image or react-native-fast-image — they handle caching, progressive loading, and memory management far better than the built-in Image.
7. Offload Heavy Work to the UI Thread
Use InteractionManager to defer expensive operations until after animations complete:
useEffect(() => {
const task = InteractionManager.runAfterInteractions(() => {
// Heavy computation, large data processing, etc.
loadLargeDataset();
});
return () => task.cancel();
}, []);
8. Profile Before Optimising
Don't guess — use the built-in tools:
- Flipper with the React DevTools plugin
- Performance Monitor (shake device → Show Perf Monitor)
- Systrace for native thread profiling
The JS thread and UI thread FPS tell you exactly where your bottleneck is. Most of the time it's one of the issues above.
Summary
| Issue | Fix | |---|---| | Bridge overhead | Enable New Architecture | | Unnecessary re-renders | memo, useCallback, useMemo | | FlatList jank | windowSize, removeClippedSubviews | | Style recreation | StyleSheet.create | | Slow startup | Enable Hermes | | Image memory | react-native-fast-image | | Animation jank | InteractionManager | | Unknown bottleneck | Profile first! |
Performance optimisation is a loop, not a checklist. Profile, fix the worst offender, repeat.