After six years of building React Native apps — from logistics platforms to meditation apps — I've seen performance fail in the same ways, over and over. This is the distilled knowledge from shipping 5 production apps.
The JS Thread Is Your Bottleneck#
React Native runs your JavaScript on a single thread. Every animation, every state update, every re-render competes for that thread. The moment you block it, your UI stutters.
The golden rule: keep the JS thread free during animations.
// ❌ Blocking animation — runs on JS thread, causes jank
const onPress = () => {
const animation = new Animated.Value(0);
Animated.timing(animation, {
toValue: 1,
duration: 300,
useNativeDriver: false,
}).start();
};// ✅ Native-driven animation
const onPress = () => {
const animation = new Animated.Value(0);
Animated.timing(animation, {
toValue: 1,
duration: 300,
useNativeDriver: true,
}).start();
};useNativeDriver: true moves the animation to the native thread entirely. No JS thread involvement. No jank.
Reanimated 3: The Right Tool for Complex Animations#
For anything beyond simple Animated API, reach for Reanimated. Worklets run on the UI thread synchronously:
import { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated';
function PressableCard() {
const scale = useSharedValue(1);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
return (
<Pressable
onPressIn={() => { scale.value = withSpring(0.96); }}
onPressOut={() => { scale.value = withSpring(1); }}
>
<Animated.View style={[styles.card, animatedStyle]}>
{/* content */}
</Animated.View>
</Pressable>
);
}The useAnimatedStyle worklet runs entirely on the UI thread — zero JS overhead.
FlatList: The Single Biggest Win#
Replacing ScrollView with FlatList on a list of 50+ items is usually an instant 2-3x improvement. But FlatList has footguns:
// ❌ Creating new function references on every render
<FlatList
data={items}
renderItem={({ item }) => <ItemRow item={item} />}
keyExtractor={(item) => item.id}
/>// ✅ Stable references
const renderItem = useCallback(
({ item }: { item: Item }) => <ItemRow item={item} />,
[]
);
const keyExtractor = useCallback((item: Item) => item.id, []);
<FlatList
data={items}
renderItem={renderItem}
keyExtractor={keyExtractor}
getItemLayout={(_, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
})}
removeClippedSubviews={true}
maxToRenderPerBatch={10}
windowSize={5}
/>getItemLayout is the secret weapon. It skips layout measurement for every item, which is expensive. If your list items have a fixed height, always implement this.
Memory Leaks in Subscription Patterns#
This one bit me hard at Maharishi — meditation timer sessions that would leak memory over time:
// ❌ Memory leak — event listener never cleaned up
useEffect(() => {
const subscription = DeviceEventEmitter.addListener('timerTick', handleTick);
// forgot cleanup!
}, []);
// ✅ Always clean up
useEffect(() => {
const subscription = DeviceEventEmitter.addListener('timerTick', handleTick);
return () => subscription.remove();
}, [handleTick]);Same applies to AppState, NetInfo, and any other subscription. The rule: if you subscribe, you must unsubscribe.
Hermes Engine#
Enable it. Now. The performance gains are significant:
// android/app/build.gradle
project.ext.react = [
enableHermes: true,
]<!-- ios/Podfile -->
use_react_native!(
:path => config[:reactNativePath],
:hermes_enabled => true
)Hermes pre-compiles JS to bytecode at build time, dramatically reducing TTI (time to interactive).
The Profiler Workflow#
Don't guess at performance problems. Profile them:
- Flipper — For general JS thread and memory profiling
- React Native Performance Monitor — Quick FPS and RAM overlay (shake device → Perf Monitor)
- Android Profiler / Xcode Instruments — For native-level issues
- why-did-you-render — For identifying unnecessary re-renders in dev
npm install @welldone-software/why-did-you-render --save-dev// src/wdyr.ts
import React from 'react';
if (process.env.NODE_ENV === 'development') {
const whyDidYouRender = require('@welldone-software/why-did-you-render');
whyDidYouRender(React, {
trackAllPureComponents: true,
});
}Key Takeaways#
- Use
useNativeDriver: truefor all Animated API animations - Use Reanimated for complex gesture-driven animations
- Always use FlatList for scrollable lists — implement
getItemLayoutif items are fixed height - Clean up all subscriptions in
useEffectreturn functions - Enable Hermes — it's a free 10-30% TTI improvement
- Profile before optimizing — gut feelings about perf bottlenecks are usually wrong