If a React Native app feels slow on iOS, the biggest gains usually come from identifying whether the bottleneck is:
- JavaScript work
- React rendering
- Native UI rendering/layout
- Bridge communication (or JSI/TurboModules interactions)
- Network or image loading
- Startup time
Here's a practical workflow.
1. Profile before optimizing
Avoid guessing. Measure first.
Xcode Instruments
Use Instruments with:
- Time Profiler — CPU hotspots
- Core Animation — dropped frames
- Allocations — memory usage
- Leaks — memory leaks
- Network — request timing
Run on a real device whenever possible.
React Native Performance Monitor
Enable the performance overlay.
Watch:
Interpretation:
- Low JS FPS → expensive JavaScript
- Low UI FPS but good JS FPS → native rendering/layout issue
React DevTools Profiler
Profile component renders.
Look for:
- Components rendering repeatedly
- Long commit times
- Large render trees
Common discovery:
App
├── Feed
├── Header
├── Footer
Changing one small state causes the whole tree to rerender.
2. Reduce unnecessary renders
This is often the largest performance improvement.
Use React.memo
const Item = React.memo(({ item }) => {
return <Row item={item} />;
});
Stable callbacks
Instead of
<Button onPress={() => doSomething(id)} />
use
const onPress = useCallback(() => {
doSomething(id);
}, [id]);
Stable objects
Bad
<View style={{ margin: 10 }} />
Better
const styles = StyleSheet.create({
box: {
margin: 10,
},
});
3. Optimize FlatList
Most performance problems involve lists.
Use
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={keyExtractor}
removeClippedSubviews
initialNumToRender={10}
maxToRenderPerBatch={10}
windowSize={5}
/>
Memoize:
const renderItem = useCallback(...);
Memoize rows:
const Row = React.memo(...)
If item height is fixed:
getItemLayout={(data, index) => ({
length: 70,
offset: 70 * index,
index,
})}
Huge improvement.
4. Reduce bridge traffic
For older architecture especially.
Avoid:
setState(...)
setState(...)
setState(...)
Batch updates.
Avoid sending huge objects repeatedly between JS and native.
5. Optimize animations
Avoid JS-driven animations.
Instead:
Bad
Animated.timing(value, {
useNativeDriver: false
})
Better
useNativeDriver: true
Or use Reanimated worklets.
6. Optimize images
Large images kill scrolling performance.
Use:
- appropriately sized images
- cached images
- modern formats (WebP, AVIF where supported)
Avoid displaying:
4000×3000
inside
100×100
7. Avoid excessive state
Instead of
const [user, setUser] = ...
where changing one field rerenders everything,
split state.
Example:
const [name]
const [avatar]
const [settings]
or use selector-based state libraries.
8. Use Hermes
Hermes significantly improves:
- startup
- memory
- JS execution
Modern React Native projects generally use Hermes by default.
9. Watch expensive effects
Bad
useEffect(() => {
expensiveCalculation();
});
Runs every render.
Better
useEffect(() => {
expensiveCalculation();
}, []);
Or
const result = useMemo(() => expensiveCalculation(), [data]);
10. Avoid synchronous work during startup
Don't load everything immediately.
Instead:
- lazy load screens
- lazy initialize SDKs
- defer analytics
- fetch after first paint
The first screen should become interactive as quickly as possible.
11. Measure startup
Useful metrics:
- Time to first screen
- Time to interactive
- JS bundle load time
- Native initialization time
On iOS:
- Instruments
- Xcode launch profiling
12. Look for layout thrashing
Avoid deeply nested views like:
View
View
View
View
View
Flatten layouts where possible.
Large layout recalculations can hurt scrolling.
13. Memory profiling
If scrolling gets slower over time:
Check for:
- retained images
- leaked timers
- event listeners
- subscriptions
- navigation stacks not released
Use Instruments → Allocations and Leaks.
14. Avoid expensive JavaScript inside render
Bad
items
.filter(...)
.sort(...)
.map(...)
every render.
Better
const visibleItems = useMemo(() => {
return items
.filter(...)
.sort(...);
}, [items]);
15. Profile production builds
Debug mode can be much slower than release.
Always compare with:
- Release build
- Real iPhone
- Production bundle
Many "performance issues" disappear outside the development environment.
Common bottlenecks and fixes
| Symptom | Likely cause | Typical fix |
|---|
| Scroll jank | Large or unoptimized FlatList | Memoize rows, tune virtualization, implement getItemLayout |
| Button taps feel delayed | Heavy JS on the main event loop | Move work off the critical path, split tasks, optimize computations |
| Animations stutter | JS-driven animations | Use the native driver or Reanimated worklets |
| High memory usage | Large images or retained objects | Resize images, release references, investigate leaks |
| Long cold start | Large bundle or eager initialization | Enable Hermes, lazy-load screens, defer nonessential SDK initialization |
| Frequent rerenders | Unstable props or broad state updates | Use React.memo, useCallback, useMemo, and isolate state |
For a deeper investigation, it helps to know:
- Which React Native version you're using (including whether you're on the New Architecture/Fabric).
- Whether the slowdown occurs during startup, scrolling, animations, navigation, or general interaction.
- Whether it reproduces only on iOS or on Android as well.