Performance in React Native
React Native apps can achieve near-native performance, but require attention to common pitfalls. The key areas are: rendering optimization, list performance, image handling, and smooth animations.
Avoiding Unnecessary Re-renders
import { memo, useCallback, useMemo } from 'react';
import { View, Text, FlatList, Pressable } from 'react-native';
// ❌ Bad: Creates new function on every render
function BadComponent({ items, onItemPress }) {
return (
<FlatList
data={items}
renderItem={({ item }) => (
<Pressable onPress={() => onItemPress(item.id)}>
<Text>{item.name}</Text>
</Pressable>
)}
/>
);
}
// ✅ Good: Memoized component and callbacks
const MemoizedItem = memo(function Item({ item, onPress }) {
return (
<Pressable onPress={() => onPress(item.id)}>
<Text>{item.name}</Text>
</Pressable>
);
});
function GoodComponent({ items, onItemPress }) {
const handlePress = useCallback((id) => {
onItemPress(id);
}, [onItemPress]);
const renderItem = useCallback(({ item }) => (
<MemoizedItem item={item} onPress={handlePress} />
), [handlePress]);
return (
<FlatList
data={items}
renderItem={renderItem}
keyExtractor={(item) => item.id}
/>
);
}
// useMemo for expensive calculations
function ExpensiveComponent({ data }) {
const processedData = useMemo(() => {
return data.map(item => ({
...item,
computedValue: expensiveCalculation(item),
}));
}, [data]);
return <List data={processedData} />;
}
FlatList Optimization
import { FlatList, View } from 'react-native';
function OptimizedList({ data }) {
const renderItem = useCallback(({ item }) => (
<MemoizedListItem item={item} />
), []);
// Estimate item height for faster scrolling
const getItemLayout = useCallback((data, index) => ({
length: 80, // item height
offset: 80 * index,
index,
}), []);
return (
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={(item) => item.id}
getItemLayout={getItemLayout}
// Performance props
removeClippedSubviews={true} // Unmount off-screen items
maxToRenderPerBatch={10} // Items per batch
updateCellsBatchingPeriod={50} // ms between batch renders
initialNumToRender={10} // Initial items to render
windowSize={5} // Render window multiplier
// Maintain scroll position
maintainVisibleContentPosition={{
minIndexForVisible: 0,
}}
/>
);
}
// For very long lists, use FlashList
// npm install @shopify/flash-list
import { FlashList } from '@shopify/flash-list';
function FastList({ data }) {
return (
<FlashList
data={data}
renderItem={({ item }) => <ListItem item={item} />}
estimatedItemSize={80}
/>
);
}
⚡ FlashList vs FlatList
FlashList by Shopify is a drop-in replacement for FlatList that's up to 10x faster. It uses cell recycling (like native lists) instead of unmounting/remounting items.
Image Optimization
import { Image } from 'react-native';
import FastImage from 'react-native-fast-image';
// ❌ Bad: No size, no caching strategy
<Image source={{ uri: imageUrl }} />
// ✅ Good: Proper sizing and caching
<Image
source={{
uri: imageUrl,
cache: 'force-cache', // iOS only
}}
style={{ width: 200, height: 200 }}
resizeMode="cover"
/>
// ✅ Better: Use FastImage for advanced caching
// npm install react-native-fast-image
<FastImage
source={{
uri: imageUrl,
priority: FastImage.priority.normal,
cache: FastImage.cacheControl.immutable,
}}
style={{ width: 200, height: 200 }}
resizeMode={FastImage.resizeMode.cover}
/>
// Preload images
FastImage.preload([
{ uri: 'https://example.com/image1.jpg' },
{ uri: 'https://example.com/image2.jpg' },
]);
// For Expo, use expo-image
import { Image } from 'expo-image';
<Image
source={imageUrl}
style={{ width: 200, height: 200 }}
contentFit="cover"
placeholder={blurhash}
transition={200}
/>
Animations with Reanimated
// npm install react-native-reanimated
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
withTiming,
interpolate,
Easing,
} from 'react-native-reanimated';
function AnimatedBox() {
const offset = useSharedValue(0);
const scale = useSharedValue(1);
// Animated styles run on UI thread (60fps)
const animatedStyles = useAnimatedStyle(() => ({
transform: [
{ translateX: offset.value },
{ scale: scale.value },
],
}));
const handlePress = () => {
// Spring animation
offset.value = withSpring(offset.value === 0 ? 100 : 0, {
damping: 15,
stiffness: 100,
});
// Timing animation
scale.value = withTiming(scale.value === 1 ? 1.2 : 1, {
duration: 300,
easing: Easing.bezier(0.25, 0.1, 0.25, 1),
});
};
return (
<Pressable onPress={handlePress}>
<Animated.View style={[styles.box, animatedStyles]} />
</Pressable>
);
}
// Scroll-based animations
function ParallaxHeader({ scrollY }) {
const headerStyle = useAnimatedStyle(() => ({
opacity: interpolate(scrollY.value, [0, 100], [1, 0]),
transform: [
{ translateY: interpolate(scrollY.value, [0, 100], [0, -50]) },
],
}));
return <Animated.View style={[styles.header, headerStyle]} />;
}
Gesture Handling
// npm install react-native-gesture-handler
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, { useSharedValue, useAnimatedStyle } from 'react-native-reanimated';
function DraggableBox() {
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const context = useSharedValue({ x: 0, y: 0 });
const gesture = Gesture.Pan()
.onStart(() => {
context.value = { x: translateX.value, y: translateY.value };
})
.onUpdate((event) => {
translateX.value = context.value.x + event.translationX;
translateY.value = context.value.y + event.translationY;
})
.onEnd(() => {
// Snap back or stay
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{ translateX: translateX.value },
{ translateY: translateY.value },
],
}));
return (
<GestureDetector gesture={gesture}>
<Animated.View style={[styles.box, animatedStyle]} />
</GestureDetector>
);
}
// Swipe to delete
function SwipeableItem({ onDelete }) {
const translateX = useSharedValue(0);
const gesture = Gesture.Pan()
.onUpdate((e) => {
translateX.value = Math.min(0, e.translationX);
})
.onEnd(() => {
if (translateX.value < -100) {
translateX.value = withTiming(-200);
runOnJS(onDelete)();
} else {
translateX.value = withSpring(0);
}
});
return (
<GestureDetector gesture={gesture}>
<Animated.View style={[styles.item, { transform: [{ translateX }] }]}>
<Text>Swipe to delete</Text>
</Animated.View>
</GestureDetector>
);
}
Debugging Performance
// Enable React DevTools Profiler
// In development, shake device > "Toggle Profiler"
// Monitor JS thread FPS
import { PerformanceMonitor } from 'react-native';
function App() {
return (
<>
{__DEV__ && <PerformanceMonitor />}
<MainApp />
</>
);
}
// Use Flipper for detailed profiling
// Install Flipper desktop app
// Detect slow renders with React DevTools
// Look for components with many renders
// Console timing
console.time('ExpensiveOperation');
// ... operation
console.timeEnd('ExpensiveOperation');
// Hermes profiling (production-like)
// npx react-native run-android --variant=release
// Then use Flipper's Hermes Debugger
Performance Checklist
| Area | Optimization |
|---|---|
| Lists | Use FlatList/FlashList, keyExtractor, getItemLayout |
| Images | Proper sizing, FastImage/expo-image, preloading |
| Components | React.memo, useCallback, useMemo |
| Animations | Reanimated (UI thread), native driver |
| Navigation | Lazy loading screens, native stack |
| Bundle | Hermes enabled, tree shaking, code splitting |
⚠️ Common Performance Mistakes
- • Creating functions/objects in render (use useCallback/useMemo)
- • Using ScrollView for long lists (use FlatList)
- • Animating with setState (use Reanimated)
- • Large images without proper sizing
- • Console.log in production builds
- • Not using Hermes JavaScript engine