Mobile App Performance Optimization: Advanced Techniques for React Native
Deep dive into performance optimization for React Native apps. Learn profiling, memory management, rendering optimization, and advanced techniques to build lightning-fast mobile apps.
Sani Mridha
Senior Mobile Developer
Mobile App Performance Optimization: Advanced Techniques for React Native
Performance can make or break your mobile app. Let's explore advanced techniques to build lightning-fast React Native applications.
Why Performance Matters
The Numbers
User Experience Impact
Performance Profiling
React DevTools Profiler
import { Profiler } from 'react';
function onRenderCallback(
id: string,
phase: "mount" | "update",
actualDuration: number,
baseDuration: number,
startTime: number,
commitTime: number
) {
console.log(`${id} took ${actualDuration}ms to render`);
}
<Profiler id="MyComponent" onRender={onRenderCallback}>
<MyComponent />
</Profiler>Flipper Performance Plugin
# Install Flipper
brew install --cask flipper
# Enable performance monitoring
# In your app, the React Native Flipper plugin is automatically enabledCustom Performance Monitoring
class PerformanceMonitor {
private metrics: Map<string, number[]> = new Map();
startMeasure(label: string) {
performance.mark(`${label}-start`);
}
endMeasure(label: string) {
performance.mark(`${label}-end`);
performance.measure(label, `${label}-start`, `${label}-end`);
const measure = performance.getEntriesByName(label)[0];
const duration = measure.duration;
if (!this.metrics.has(label)) {
this.metrics.set(label, []);
}
this.metrics.get(label)!.push(duration);
// Log if slow
if (duration > 16.67) { // 60 FPS threshold
console.warn(`Slow operation: ${label} took ${duration.toFixed(2)}ms`);
}
}
getStats(label: string) {
const durations = this.metrics.get(label) || [];
return {
avg: durations.reduce((a, b) => a + b, 0) / durations.length,
min: Math.min(...durations),
max: Math.max(...durations),
count: durations.length
};
}
}
const monitor = new PerformanceMonitor();
// Usage
monitor.startMeasure('data-fetch');
await fetchData();
monitor.endMeasure('data-fetch');Rendering Optimization
1. Memoization
import { memo, useMemo, useCallback } from 'react';
// Memoize components
const ExpensiveComponent = memo(({ data, onPress }) => {
return (
<View>
{data.map(item => <Item key={item.id} {...item} />)}
</View>
);
}, (prevProps, nextProps) => {
// Custom comparison
return prevProps.data.length === nextProps.data.length;
});
// Memoize computed values
function MyComponent({ items }) {
const sortedItems = useMemo(() => {
return items.sort((a, b) => a.name.localeCompare(b.name));
}, [items]);
const handlePress = useCallback((id: string) => {
// Handle press
}, []);
return <List items={sortedItems} onPress={handlePress} />;
}2. FlatList Optimization
import { FlatList } from 'react-native';
<FlatList
data={items}
renderItem={renderItem}
keyExtractor={(item) => item.id}
// Performance optimizations
removeClippedSubviews={true}
maxToRenderPerBatch={10}
updateCellsBatchingPeriod={50}
initialNumToRender={10}
windowSize={5}
// Use getItemLayout for fixed-height items
getItemLayout={(data, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
})}
// Optimize re-renders
extraData={selectedId}
/>3. Image Optimization
import FastImage from 'react-native-fast-image';
// Use FastImage instead of Image
<FastImage
source={{
uri: imageUrl,
priority: FastImage.priority.normal,
cache: FastImage.cacheControl.immutable,
}}
resizeMode={FastImage.resizeMode.cover}
style={styles.image}
/>
// Preload images
FastImage.preload([
{ uri: 'https://example.com/image1.jpg' },
{ uri: 'https://example.com/image2.jpg' },
]);
// Progressive image loading
const [imageLoaded, setImageLoaded] = useState(false);
<View>
{!imageLoaded && <Skeleton />}
<FastImage
source={{ uri: imageUrl }}
onLoadEnd={() => setImageLoaded(true)}
style={[styles.image, { opacity: imageLoaded ? 1 : 0 }]}
/>
</View>Memory Management
1. Cleanup Side Effects
useEffect(() => {
const subscription = eventEmitter.addListener('event', handler);
const interval = setInterval(updateData, 1000);
return () => {
// Cleanup
subscription.remove();
clearInterval(interval);
};
}, []);2. Avoid Memory Leaks
// Bad: Creates new function on every render
<Button onPress={() => handlePress(item.id)} />
// Good: Use useCallback
const handlePress = useCallback((id: string) => {
// Handle press
}, []);
<Button onPress={() => handlePress(item.id)} />
// Better: Pass stable reference
const handlePressFactory = useCallback((id: string) => () => {
// Handle press
}, []);
<Button onPress={handlePressFactory(item.id)} />3. Large Lists Memory Management
import { RecyclerListView, DataProvider, LayoutProvider } from 'recyclerlistview';
// RecyclerListView recycles views for better memory usage
const dataProvider = new DataProvider((r1, r2) => r1.id !== r2.id);
const layoutProvider = new LayoutProvider(
(index) => 'ITEM',
(type, dim) => {
dim.width = width;
dim.height = 100;
}
);
<RecyclerListView
dataProvider={dataProvider.cloneWithRows(items)}
layoutProvider={layoutProvider}
rowRenderer={renderRow}
/>Network Optimization
1. Request Batching
class RequestBatcher {
private queue: Array<{
endpoint: string;
resolve: (data: any) => void;
reject: (error: any) => void;
}> = [];
private timeout: NodeJS.Timeout | null = null;
request(endpoint: string): Promise<any> {
return new Promise((resolve, reject) => {
this.queue.push({ endpoint, resolve, reject });
if (this.timeout) {
clearTimeout(this.timeout);
}
this.timeout = setTimeout(() => this.flush(), 50);
});
}
private async flush() {
const batch = this.queue.splice(0);
try {
const response = await fetch('/api/batch', {
method: 'POST',
body: JSON.stringify({
requests: batch.map(r => r.endpoint)
})
});
const results = await response.json();
batch.forEach((item, index) => {
item.resolve(results[index]);
});
} catch (error) {
batch.forEach(item => item.reject(error));
}
}
}
const batcher = new RequestBatcher();
// Usage
const data1 = await batcher.request('/api/user/1');
const data2 = await batcher.request('/api/user/2');
// Both requests are batched into a single network call2. Caching Strategy
import AsyncStorage from '@react-native-async-storage/async-storage';
class CacheManager {
private memoryCache = new Map<string, { data: any; timestamp: number }>();
private readonly TTL = 5 * 60 * 1000; // 5 minutes
async get(key: string): Promise<any | null> {
// Check memory cache first
const memCached = this.memoryCache.get(key);
if (memCached && Date.now() - memCached.timestamp < this.TTL) {
return memCached.data;
}
// Check persistent cache
try {
const cached = await AsyncStorage.getItem(key);
if (cached) {
const { data, timestamp } = JSON.parse(cached);
if (Date.now() - timestamp < this.TTL) {
this.memoryCache.set(key, { data, timestamp });
return data;
}
}
} catch (error) {
console.error('Cache read error:', error);
}
return null;
}
async set(key: string, data: any): Promise<void> {
const timestamp = Date.now();
// Set memory cache
this.memoryCache.set(key, { data, timestamp });
// Set persistent cache
try {
await AsyncStorage.setItem(key, JSON.stringify({ data, timestamp }));
} catch (error) {
console.error('Cache write error:', error);
}
}
clear() {
this.memoryCache.clear();
AsyncStorage.clear();
}
}
const cache = new CacheManager();
// Usage
async function fetchUserData(userId: string) {
const cacheKey = `user_${userId}`;
// Try cache first
const cached = await cache.get(cacheKey);
if (cached) return cached;
// Fetch from network
const data = await fetch(`/api/users/${userId}`).then(r => r.json());
// Cache the result
await cache.set(cacheKey, data);
return data;
}3. Optimistic Updates
function useLikePost() {
const [isLiked, setIsLiked] = useState(false);
const [likeCount, setLikeCount] = useState(0);
const toggleLike = async (postId: string) => {
// Optimistic update
const previousLiked = isLiked;
const previousCount = likeCount;
setIsLiked(!isLiked);
setLikeCount(prev => prev + (isLiked ? -1 : 1));
try {
await fetch(`/api/posts/${postId}/like`, {
method: isLiked ? 'DELETE' : 'POST'
});
} catch (error) {
// Rollback on error
setIsLiked(previousLiked);
setLikeCount(previousCount);
showError('Failed to update like');
}
};
return { isLiked, likeCount, toggleLike };
}Animation Performance
1. Use Native Driver
import { Animated } from 'react-native';
const fadeAnim = useRef(new Animated.Value(0)).current;
// Always use native driver when possible
Animated.timing(fadeAnim, {
toValue: 1,
duration: 300,
useNativeDriver: true, // ✅ Runs on native thread
}).start();
// For transforms and opacity only
Animated.parallel([
Animated.timing(fadeAnim, {
toValue: 1,
useNativeDriver: true,
}),
Animated.timing(translateY, {
toValue: 0,
useNativeDriver: true,
}),
]).start();2. Reanimated for Complex Animations
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
withTiming,
} from 'react-native-reanimated';
function AnimatedComponent() {
const offset = useSharedValue(0);
const animatedStyles = useAnimatedStyle(() => ({
transform: [{ translateX: offset.value }],
}));
const handlePress = () => {
offset.value = withSpring(100);
};
return (
<Animated.View style={animatedStyles}>
<TouchableOpacity onPress={handlePress}>
<Text>Animate</Text>
</TouchableOpacity>
</Animated.View>
);
}3. Gesture Handler
import { GestureDetector, Gesture } from 'react-native-gesture-handler';
import Animated, { useSharedValue, useAnimatedStyle } from 'react-native-reanimated';
function DraggableBox() {
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const pan = Gesture.Pan()
.onChange((event) => {
translateX.value += event.changeX;
translateY.value += event.changeY;
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{ translateX: translateX.value },
{ translateY: translateY.value },
],
}));
return (
<GestureDetector gesture={pan}>
<Animated.View style={[styles.box, animatedStyle]} />
</GestureDetector>
);
}Bundle Size Optimization
1. Code Splitting
// Lazy load screens
const ProfileScreen = lazy(() => import('./screens/ProfileScreen'));
const SettingsScreen = lazy(() => import('./screens/SettingsScreen'));
// Use Suspense
<Suspense fallback={<LoadingScreen />}>
<ProfileScreen />
</Suspense>2. Remove Unused Dependencies
# Analyze bundle
npx react-native-bundle-visualizer
# Remove unused dependencies
npm uninstall unused-package
# Use smaller alternatives
# Instead of moment (67KB), use date-fns (13KB)
npm install date-fns3. Hermes Engine
// android/app/build.gradle
project.ext.react = [
enableHermes: true // ✅ Enable Hermes
]
// Benefits:
// - Faster app startup
// - Reduced memory usage
// - Smaller app sizeStartup Performance
1. Reduce Initial Bundle
// Split into multiple bundles
// metro.config.js
module.exports = {
serializer: {
createModuleIdFactory: () => {
return (path) => {
// Create stable module IDs for better caching
return hash(path);
};
},
},
};2. Lazy Initialization
// Don't initialize everything on startup
class Analytics {
private static instance: Analytics | null = null;
static getInstance() {
if (!this.instance) {
this.instance = new Analytics();
}
return this.instance;
}
private constructor() {
// Heavy initialization
}
}
// Initialize only when needed
const analytics = Analytics.getInstance();3. Splash Screen Optimization
import SplashScreen from 'react-native-splash-screen';
function App() {
useEffect(() => {
// Hide splash screen after app is ready
const prepare = async () => {
await loadCriticalData();
SplashScreen.hide();
};
prepare();
}, []);
return <AppContent />;
}Monitoring in Production
1. Performance Metrics
import { PerformanceObserver } from 'react-native-performance';
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
// Send to analytics
analytics.track('performance', {
name: entry.name,
duration: entry.duration,
startTime: entry.startTime,
});
});
});
observer.observe({ entryTypes: ['measure', 'navigation'] });2. Crash Reporting
import crashlytics from '@react-native-firebase/crashlytics';
// Log errors
crashlytics().log('User performed action');
// Record errors
try {
// risky operation
} catch (error) {
crashlytics().recordError(error);
}
// Set user attributes
crashlytics().setUserId(userId);
crashlytics().setAttribute('role', 'premium');3. Custom Metrics
import perf from '@react-native-firebase/perf';
async function fetchUserData() {
const trace = await perf().startTrace('user_data_fetch');
try {
const data = await fetch('/api/user');
trace.putAttribute('cache_hit', 'false');
return data;
} finally {
await trace.stop();
}
}Conclusion
Performance optimization is an ongoing process:
1. Measure first - Profile before optimizing
2. Focus on impact - Optimize the biggest bottlenecks
3. Test on real devices - Emulators don't show real performance
4. Monitor production - Track metrics in the wild
5. Iterate continuously - Performance degrades over time
Remember: Premature optimization is the root of all evil. Optimize when you have data showing it's needed!
---
*Need help optimizing your React Native app? Let's chat!*