Back to Posts
Optimizing React Native Performance
By Lumina Software•
react-nativeperformancemobile-developmentexpo
Optimizing React Native Performance
Performance is critical for mobile apps. Users expect smooth animations, fast load times, and responsive interactions. Here's how to optimize React Native apps for peak performance.
Performance Metrics That Matter
Key Indicators
- Time to Interactive (TTI): When app becomes usable
- Frame Rate: Target 60 FPS for smooth animations
- Bundle Size: Smaller = faster downloads
- Memory Usage: Prevent crashes and slowdowns
- Startup Time: First impression matters
Rendering Optimization
1. Use FlashList Instead of FlatList
FlashList is significantly faster:
import { FlashList } from '@shopify/flash-list';
<FlashList
data={items}
renderItem={renderItem}
estimatedItemSize={100} // Critical for performance
onEndReached={loadMore}
onEndReachedThreshold={0.5}
/>
Key differences:
- Better recycling of views
- More accurate size estimation
- Improved scroll performance
2. Memoize Expensive Components
import { memo, useMemo } from 'react';
// Memoize component
const ExpensiveComponent = memo(({ data }) => {
const processed = useMemo(
() => expensiveComputation(data),
[data]
);
return <View>{processed}</View>;
});
// Memoize callbacks
const handlePress = useCallback(() => {
doSomething(id);
}, [id]);
3. Avoid Inline Functions and Objects
// ❌ Bad: Creates new function/object on every render
<Button onPress={() => handlePress(id)} style={{ margin: 10 }} />
// ✅ Good: Stable references
const handlePress = useCallback(() => handlePress(id), [id]);
const buttonStyle = useMemo(() => ({ margin: 10 }), []);
<Button onPress={handlePress} style={buttonStyle} />
4. Use React.memo Wisely
// Only memoize if props change infrequently
const UserCard = memo(({ user }) => {
return <View>...</View>;
}, (prevProps, nextProps) => {
// Custom comparison
return prevProps.user.id === nextProps.user.id;
});
Image Optimization
Use expo-image
import { Image } from 'expo-image';
<Image
source={{ uri: 'https://example.com/image.jpg' }}
placeholder={blurhash} // Show placeholder while loading
contentFit="cover"
transition={200} // Smooth fade-in
cachePolicy="memory-disk" // Cache strategy
/>
Image Sizing
// Always specify dimensions
<Image
source={source}
style={{ width: 200, height: 200 }} // Prevents layout shifts
contentFit="cover"
/>
Lazy Loading
import { useInView } from 'react-native-intersection-observer';
function LazyImage({ source }) {
const { ref, inView } = useInView();
return (
<View ref={ref}>
{inView ? (
<Image source={source} />
) : (
<Placeholder />
)}
</View>
);
}
Bundle Size Optimization
1. Code Splitting
// Lazy load heavy components
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<Loading />}>
<HeavyComponent />
</Suspense>
);
}
2. Tree Shaking
// ✅ Good: Import only what you need
import { debounce } from 'lodash-es';
// ❌ Bad: Imports entire library
import _ from 'lodash';
3. Analyze Bundle
# Analyze bundle size
npx react-native-bundle-visualizer
# Or with Expo
eas build --profile production --analyze
4. Remove Unused Dependencies
# Find unused dependencies
npx depcheck
# Remove unused packages
npm uninstall unused-package
Memory Management
1. Clean Up Subscriptions
useEffect(() => {
const subscription = eventEmitter.subscribe(handleEvent);
return () => {
subscription.unsubscribe(); // Cleanup
};
}, []);
2. Avoid Memory Leaks
// ❌ Bad: Storing references
const [data, setData] = useState([]);
useEffect(() => {
fetchData().then(setData);
// data array keeps growing, never cleared
}, []);
// ✅ Good: Clear old data
useEffect(() => {
let cancelled = false;
fetchData().then(newData => {
if (!cancelled) {
setData(newData);
}
});
return () => {
cancelled = true;
};
}, []);
3. Use Weak References
// For caching that shouldn't prevent GC
const cache = new WeakMap();
function getCached(key: object) {
if (!cache.has(key)) {
cache.set(key, expensiveComputation(key));
}
return cache.get(key);
}
Network Optimization
1. Request Batching
// ❌ Bad: Multiple requests
const user = await fetchUser(id);
const posts = await fetchPosts(userId);
const comments = await fetchComments(postId);
// ✅ Good: Batch requests
const [user, posts, comments] = await Promise.all([
fetchUser(id),
fetchPosts(userId),
fetchComments(postId),
]);
2. Request Deduplication
// Use TanStack Query for automatic deduplication
const { data } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
});
3. Optimistic Updates
const mutation = useMutation({
mutationFn: updatePost,
onMutate: async (newPost) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['posts'] });
// Optimistically update
queryClient.setQueryData(['posts'], (old) => [
...old,
newPost,
]);
},
});
Animation Performance
1. Use Native Driver
import { Animated } from 'react-native';
const animValue = useRef(new Animated.Value(0)).current;
Animated.timing(animValue, {
toValue: 1,
duration: 300,
useNativeDriver: true, // ✅ Runs on UI thread
}).start();
2. Use Reanimated 3
import Animated, { useSharedValue, withSpring } from 'react-native-reanimated';
const translateX = useSharedValue(0);
const handlePress = () => {
translateX.value = withSpring(100); // Smooth spring animation
};
3. Avoid Layout Animations
// ❌ Bad: Triggers layout recalculation
Animated.timing(animValue, {
toValue: 1,
useNativeDriver: false, // Runs on JS thread
});
// ✅ Good: Transform only
Animated.timing(animValue, {
toValue: 1,
useNativeDriver: true, // Transform properties only
});
Startup Optimization
1. Lazy Load Initial Screen
// Load critical content first
function App() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name="Home"
component={lazy(() => import('./screens/Home'))}
/>
</Stack.Navigator>
</NavigationContainer>
);
}
2. Preload Critical Data
// Prefetch data before navigation
useEffect(() => {
queryClient.prefetchQuery({
queryKey: ['user'],
queryFn: fetchUser,
});
}, []);
3. Reduce Initial Bundle
// Split vendor bundles
// next.config.js or metro.config.js
module.exports = {
// Split code into chunks
};
Profiling Tools
React Native Performance Monitor
import { PerformanceMonitor } from 'react-native-performance-monitor';
<PerformanceMonitor
onMetrics={(metrics) => {
console.log('FPS:', metrics.fps);
console.log('Memory:', metrics.memory);
}}
/>
Flipper Integration
// Use Flipper for debugging
// - Network inspector
// - Layout inspector
// - Performance profiler
Chrome DevTools
# Enable remote debugging
# Use Chrome DevTools for profiling
Best Practices Checklist
- Use FlashList for long lists
- Memoize expensive computations
- Avoid inline functions/objects
- Optimize images with expo-image
- Code split heavy components
- Clean up subscriptions
- Use native driver for animations
- Batch network requests
- Monitor performance metrics
- Test on real devices
Measuring Performance
Performance Monitoring
import { performance } from 'perf_hooks';
const start = performance.now();
await expensiveOperation();
const duration = performance.now() - start;
console.log(`Operation took ${duration}ms`);
Real User Monitoring
// Track real user metrics
analytics.track('app_startup_time', {
duration: startupTime,
device: deviceInfo,
});
Conclusion
Performance optimization is an ongoing process. Key principles:
- Measure first: Know your baseline
- Optimize bottlenecks: Focus on biggest wins
- Test on real devices: Simulators lie
- Monitor continuously: Track metrics over time
- Iterate: Performance is never "done"
With these techniques, your React Native app will feel fast, smooth, and responsive—exactly what users expect.
