React Native Performance Optimization
A well-written React Native app runs at 60 frames per second. Every frame takes 16 milliseconds. When any operation blocks that 16ms window, the screen stutters. This topic covers the most impactful techniques to keep your app fast and responsive.
Understanding the Two Threads
JavaScript Thread Native (UI) Thread
──────────────────────────────────────────────────────────
Runs your React code Renders pixels on screen
Handles state, logic, events Runs animations
Communicates over the bridge Must stay at 60fps
If your JS thread is busy:
└── bridge is delayed
└── native thread waits
└── animation stutters ← user sees this
The Most Common Performance Mistakes
Mistake Fix ────────────────────────────────────────────────────────────── Inline style objects Use StyleSheet.create Anonymous functions in render useCallback Rendering all list items at once Use FlatList (not ScrollView) Missing keyExtractor on FlatList Always add keyExtractor Large images, not resized Use correct image dimensions Re-rendering the whole tree Memoize components (React.memo) Importing unused libraries Tree-shaking and lazy imports
React.memo — Skip Unnecessary Re-renders
When a parent re-renders, all its children re-render by default — even children whose props did not change. Wrap a component in React.memo to skip re-renders when its props are unchanged.
// Without memo: re-renders every time parent re-renders
function ContactRow({ name, phone }) {
return (
<View>
<Text>{name}</Text>
<Text>{phone}</Text>
</View>
);
}
// With memo: skips re-render if name and phone didn't change
const ContactRow = React.memo(function ContactRow({ name, phone }) {
return (
<View>
<Text>{name}</Text>
<Text>{phone}</Text>
</View>
);
});
Without React.memo: With React.memo: Parent renders Parent renders ↓ ↓ ContactRow re-renders ✗ Did props change? ContactRow re-renders ✗ → No → skip ✓ ContactRow re-renders ✗ → Yes → re-render ✓ (wasted work) (only when needed)
useCallback — Stable Function References
Every render creates a new function in memory. Passing a new function as a prop breaks React.memo because the prop technically changed. useCallback keeps the same function reference between renders.
// Bad: new function created on every render, breaks React.memo on ContactRow
function ContactList({ contacts }) {
function handlePress(id) {
navigation.navigate('Detail', { id });
}
return <FlatList data={contacts} renderItem={({ item }) => <ContactRow onPress={handlePress} .../>} />;
}
// Good: same function reference unless navigation changes
function ContactList({ contacts }) {
const handlePress = useCallback((id) => {
navigation.navigate('Detail', { id });
}, [navigation]);
return <FlatList data={contacts} renderItem={({ item }) => <ContactRow onPress={handlePress} .../>} />;
}
FlatList Optimisations
<FlatList
data={items}
keyExtractor={(item) => item.id}
renderItem={renderItem}
// Performance props:
removeClippedSubviews={true} // unmount items far off screen (Android)
maxToRenderPerBatch={10} // render 10 items per batch
initialNumToRender={8} // render only 8 items on first load
windowSize={5} // keep 5 screen-heights worth of items in memory
getItemLayout={(data, index) => ({
length: 80, // fixed row height in pixels
offset: 80 * index, // position of this row
index,
})}
// getItemLayout eliminates measuring overhead — use it when all rows are the same height
/>
Image Optimisation
// Bad: loads a 4000×3000 image and shrinks it with CSS
<Image source={{ uri: 'https://example.com/huge-photo.jpg' }} style={{ width: 80, height: 80 }} />
// Good: request a thumbnail-sized image from the server
<Image source={{ uri: 'https://example.com/photo-80x80.jpg' }} style={{ width: 80, height: 80 }} />
Use FastImage from react-native-fast-image for better image caching and loading speed in production apps.
npm install react-native-fast-image
import FastImage from 'react-native-fast-image';
<FastImage
source={{
uri: 'https://example.com/photo.jpg',
priority: FastImage.priority.normal,
}}
style={{ width: 80, height: 80, borderRadius: 40 }}
resizeMode={FastImage.resizeMode.cover}
/>
Lazy Loading Screens
Tab navigators load all tab screens at startup by default. Set lazy to true so each tab only loads when the user visits it.
<Tab.Navigator screenOptions={{ lazy: true }}>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen name="Search" component={SearchScreen} />
<Tab.Screen name="Profile" component={ProfileScreen} />
</Tab.Navigator>
Hermes Engine
Hermes is a JavaScript engine built by Meta specifically for React Native. It pre-compiles JavaScript at build time, which reduces startup time and memory usage. Expo enables Hermes by default in modern versions.
// app.json — verify Hermes is enabled
{
"expo": {
"android": { "jsEngine": "hermes" },
"ios": { "jsEngine": "hermes" }
}
}
Profiling with Flipper and React DevTools
Never guess at performance issues — measure them. Use React DevTools Profiler to see which components re-render too often and how long each render takes.
1. Install React Native Debugger or connect Flipper 2. Open Components tab → find components highlighted in yellow (re-rendered) 3. Open Profiler tab → record a session → see render times Flame chart: ┌─────────────────────────────────────────────┐ │ App ████░░░░░░░░░░░░ 4.2ms │ │ FlatList ████░░ 2.1ms │ │ ContactRow █ 0.3ms ← fast, green │ │ ContactRow █ 0.3ms │ │ ContactRow ████████ 3.5ms ← slow, red │ └─────────────────────────────────────────────┘
Avoiding the Most Expensive Operations
Operation Cost Alternative ──────────────────────────────────────────────────────────────── Parsing large JSON in render High Parse in useEffect Heavy calculations every render High useMemo Network calls in renderItem High Pre-fetch data before rendering Shadows on Android Medium Use elevation, limit shadow views Transparent backgrounds Medium Set opaque background where possible Setting state inside loops High Batch into one setState call
InteractionManager — After Animations
Run expensive operations after the current animation or transition completes, so the UI stays responsive during the transition.
import { InteractionManager } from 'react-native';
useEffect(() => {
// Wait until screen transition animation finishes
InteractionManager.runAfterInteractions(() => {
// Now safe to run expensive work
loadHeavyData();
});
}, []);
Summary
Keep the JavaScript thread free for UI work. Wrap stable components in React.memo and stable functions in useCallback. Always use FlatList for lists and add getItemLayout for fixed-height rows. Size images correctly at the source. Enable lazy loading for tabs. Use Hermes in production builds. Profile with React DevTools before optimising — measure first, then fix.
