A client brought me in after their React Native app had shipped with serious performance issues — jank on scroll, slow navigation, and an 8-second cold start. Within two weeks we had cut load time by 40% and eliminated most of the jank. Here's exactly what worked, and what didn't.
1. Enable Hermes (If You Haven't)
Hermes is now the default JavaScript engine for React Native, but older projects may still be running JSC. Hermes pre-compiles JavaScript to bytecode at build time, cutting startup time significantly. Check your android/app/build.gradle — hermesEnabled should be true. For Expo, it's enabled by default since SDK 48.
On the app that was crashing: switching from JSC to Hermes alone cut cold start from 8 seconds to 4.8 seconds. That's 3 seconds for a config change.
2. Use the New Architecture (Fabric + JSI)
React Native's New Architecture removes the async JavaScript bridge that was the root cause of UI thread jank. Fabric renders UI synchronously, JSI allows direct JS-to-native method calls. As of React Native 0.73+, you can enable it with a single flag. Most popular libraries now support it.
// android/gradle.properties
newArchEnabled=true
// ios/Podfile — set before pod install
ENV['RCT_NEW_ARCH_ENABLED'] = '1'3. Fix Your FlatList — It's Probably Wrong
The most common source of scroll jank I see is a misconfigured FlatList. Three changes make a dramatic difference:
<FlatList
data={items}
keyExtractor={(item) => item.id}
renderItem={renderItem}
// These three are critical:
removeClippedSubviews={true}
maxToRenderPerBatch={10}
windowSize={5}
// For images: set explicit dimensions so layout doesn't recalculate
getItemLayout={(_, index) => ({ length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index })}
/>getItemLayout is the biggest win if your list items have fixed height. Without it, React Native has to measure every item to calculate scroll position. With it, scroll jumps to the exact pixel immediately.
4. Memoize Aggressively in List Items
Every time the parent component re-renders, renderItem recreates. Wrap list item components in React.memo and use useCallback for handlers. This prevents the entire visible list from re-rendering when unrelated state changes.
const ListItem = React.memo(({ item, onPress }: Props) => {
return <TouchableOpacity onPress={() => onPress(item.id)}>...</TouchableOpacity>;
});
// In the parent:
const renderItem = useCallback(({ item }) => (
<ListItem item={item} onPress={handlePress} />
), [handlePress]);
const handlePress = useCallback((id: string) => {
// handle
}, []);5. Move Animations to the UI Thread
React Native's Animated API runs on the JS thread by default. When JS is busy (fetching, processing), animations stutter. Use Reanimated 3 with worklets — animations run on the native UI thread and are immune to JS thread congestion.
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated';
function PressableCard() {
const scale = useSharedValue(1);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
return (
<Animated.View style={animatedStyle}>
<Pressable
onPressIn={() => { scale.value = withSpring(0.96); }}
onPressOut={() => { scale.value = withSpring(1); }}
>
{/* content */}
</Pressable>
</Animated.View>
);
}6. Lazy Load Screens and Heavy Components
Don't import every screen at the top of your navigator. React Navigation supports lazy loading — screens are only imported when first visited. For heavy components (charts, maps, rich text editors), use dynamic imports with React.lazy and a Suspense fallback.
The app I optimized was importing a PDF viewer library on cold start even though 95% of users never opened PDFs. Lazy loading it cut the initial bundle parse time by 1.2 seconds.
7. Profile Before Optimizing Anything
The biggest waste of time I see is developers optimizing things that aren't bottlenecks. Before touching anything, use Flipper with the React DevTools and Performance plugins. Identify where frames are dropping and what's causing re-renders. React Native's built-in Performance Monitor (shake device → "Perf Monitor") shows JS and UI FPS in real time. Optimize the thing the profiler points to — not the thing that feels slow.
- Flipper + React DevTools: find unnecessary re-renders
- Flipper Network: spot slow API calls blocking render
- Xcode Instruments (iOS): find memory leaks and CPU spikes
- Android Studio Profiler: same for Android
- React Native Performance Monitor: quick sanity check on FPS
Performance work is satisfying when you measure before and after. Every change I listed above came from profiling data, not intuition. Start there.