React Native performance profiling — finding the real bottleneck
React Native performance problems hide well. The app feels sluggish but the JS frame rate looks fine. Lists scroll smoothly on your device but choppily on a mid-range Android. The Xcode profiler shows CPU spikes but you cannot map them back to your components. Getting to the real bottleneck requires using the right tools in the right order. Here is the sequence I follow.
Step 1: Establish a baseline with the frame rate overlay
Before profiling, enable the frame rate overlay to confirm you actually have a problem and identify which thread is struggling:
// In your app entry point (dev builds only)
import { PerformanceObserver } from 'react-native';
if (__DEV__) {
// Enable performance monitor overlay
// Shake device → Performance Monitor → enabled
// Or programmatically:
const { NativeModules } = require('react-native');
NativeModules.PerfMonitor?.start();
}
The overlay shows two numbers: JS FPS and UI FPS. If UI FPS drops but JS FPS stays high, the problem is on the native side (too many view updates, GPU overdraw). If JS FPS drops, the problem is in your JavaScript (heavy renders, synchronous operations).
Step 2: React DevTools profiler for component render cost
Open Flipper → React DevTools → Profiler. Record a 10-second session of normal app usage including the sluggish interaction. The flame chart shows:
- Which components rendered on each frame
- How long each render took
- Whether a render was "committed" (actually caused a DOM change) or "deferred"
The five issues I find in almost every app I audit:
Issue 1: Missing list key optimization
// BAD: re-renders every item on list data change
<FlatList
data={items}
renderItem={({ item }) => <ItemCard item={item} />}
keyExtractor={(item) => item.id}
/>
// GOOD: memoize renderItem to prevent unnecessary re-renders
const renderItem = useCallback(
({ item }: { item: Item }) => <ItemCard item={item} />,
[]
);
<FlatList
data={items}
renderItem={renderItem}
keyExtractor={(item) => item.id}
removeClippedSubviews={true}
maxToRenderPerBatch={10}
windowSize={5}
/>
Issue 2: Inline object/function props breaking memo
// BAD: new object on every parent render = memo never works
<ItemCard
style={{ marginBottom: 8 }} // new object every render
onPress={() => handlePress(item.id)} // new function every render
/>
// GOOD: stable references
const cardStyle = useMemo(() => ({ marginBottom: 8 }), []);
const handlePress = useCallback((id: string) => { /* ... */ }, []);
<ItemCard
style={cardStyle}
onPress={() => handlePress(item.id)}
/>
Issue 3: Context triggering excessive renders
// BAD: all consumers re-render when ANY part of context changes
const AppContext = createContext({ user, theme, notifications, settings });
// GOOD: split into granular contexts
const UserContext = createContext(user);
const ThemeContext = createContext(theme);
// Components only subscribe to what they need
Issue 4: Image loading on the main thread
// BAD: unoptimized images block the UI thread
<Image source={{ uri: imageUrl }} />
// GOOD: use expo-image (faster, better caching, blurhash placeholder)
import { Image } from 'expo-image';
<Image
source={imageUrl}
placeholder={item.blurhash}
contentFit="cover"
transition={200}
cachePolicy="memory-disk"
/>
Issue 5: JavaScript executing during scroll
// BAD: onScroll fires JS code every frame during scroll
<ScrollView
onScroll={(event) => {
setScrollY(event.nativeEvent.contentOffset.y); // JS setState at 60fps
}}
scrollEventThrottle={16}
/>
// GOOD: use Reanimated's useAnimatedScrollHandler (runs on UI thread)
import { useAnimatedScrollHandler, useSharedValue } from 'react-native-reanimated';
const scrollY = useSharedValue(0);
const scrollHandler = useAnimatedScrollHandler({
onScroll: (event) => {
scrollY.value = event.contentOffset.y; // UI thread only, no JS
},
});
<Animated.ScrollView onScroll={scrollHandler} scrollEventThrottle={16} />
Step 3: Hermes profiler for JS execution cost
When DevTools profiler shows expensive JS but you cannot isolate which code:
- Enable Hermes in
app.json:"jsEngine": "hermes" - Open React Native DevTools → Performance tab
- Record while triggering the slow interaction
- Look for long tasks (orange bars) — these are JS blocking the main thread
Common culprits found via Hermes profiler:
- Parsing large JSON responses synchronously
- Sorting or filtering large arrays on every render instead of memoizing
- Third-party libraries doing heavy initialisation on import
The profiling order
- Frame rate overlay → identify which thread is slow
- React DevTools Profiler → find expensive component renders
- Hermes Profiler → find expensive JS operations
- Flipper Network plugin → find slow API calls that block perceived performance
- Flipper Layout inspector → find deep view hierarchies causing GPU overdraw
In my experience, 80% of React Native performance problems are fixed by the five issues listed above — mostly unnecessary re-renders and unoptimised lists. Profile first, optimise second. Guessing wastes a day.