React Native Performance
Use this skill when optimizing React Native applications for better performance, faster load times, and smoother user experiences.
Key Concepts
List Performance
Optimize FlatList for large datasets:
import React, { useCallback } from 'react'; import { FlatList, View, Text, StyleSheet } from 'react-native';
interface Item { id: string; title: string; }
const ItemComponent = React.memo(({ item }: { item: Item }) => ( <View style={styles.item}> <Text>{item.title}</Text> </View> ));
function OptimizedList({ data }: { data: Item[] }) { const renderItem = useCallback( ({ item }: { item: Item }) => <ItemComponent item={item} />, [] );
const keyExtractor = useCallback((item: Item) => item.id, []);
const getItemLayout = useCallback( (data: any, index: number) => ({ length: 80, // Fixed item height offset: 80 * index, index, }), [] );
return ( <FlatList data={data} renderItem={renderItem} keyExtractor={keyExtractor} getItemLayout={getItemLayout} // Performance optimizations removeClippedSubviews={true} maxToRenderPerBatch={10} updateCellsBatchingPeriod={50} initialNumToRender={10} windowSize={5} /> ); }
const styles = StyleSheet.create({ item: { height: 80, padding: 16, borderBottomWidth: 1, borderBottomColor: '#eee', }, });
Component Memoization
Use React.memo and useMemo:
import React, { useMemo } from 'react'; import { View, Text } from 'react-native';
interface UserCardProps { user: { id: string; name: string; email: string; }; onPress: () => void; }
// Memoize component to prevent unnecessary re-renders const UserCard = React.memo(({ user, onPress }: UserCardProps) => { return ( <View> <Text>{user.name}</Text> <Text>{user.email}</Text> </View> ); });
// Memoize expensive computations function UserList({ users }: { users: User[] }) { const sortedUsers = useMemo(() => { return [...users].sort((a, b) => a.name.localeCompare(b.name)); }, [users]);
return ( <View> {sortedUsers.map(user => ( <UserCard key={user.id} user={user} onPress={() => {}} /> ))} </View> ); }
Image Optimization
Optimize image loading and caching:
import React from 'react'; import { Image, View } from 'react-native'; import FastImage from 'react-native-fast-image';
// Use FastImage for better performance function OptimizedImage({ uri }: { uri: string }) { return ( <FastImage style={{ width: 200, height: 200 }} source={{ uri, priority: FastImage.priority.normal, cache: FastImage.cacheControl.immutable, }} resizeMode={FastImage.resizeMode.cover} /> ); }
// Lazy load images function LazyImage({ uri }: { uri: string }) { return ( <Image source={{ uri }} style={{ width: 200, height: 200 }} resizeMode="cover" loadingIndicatorSource={require('./placeholder.png')} /> ); }
Best Practices
Use useCallback for Event Handlers
Prevent unnecessary re-renders:
import React, { useCallback, useState } from 'react'; import { View, Button, Text } from 'react-native';
function Counter() { const [count, setCount] = useState(0);
// Bad - Creates new function on every render // const increment = () => setCount(count + 1);
// Good - Memoized callback const increment = useCallback(() => { setCount(c => c + 1); }, []);
return ( <View> <Text>{count}</Text> <Button title="Increment" onPress={increment} /> </View> ); }
Virtualized Lists Only
Use FlatList/SectionList for scrollable content:
// Bad - ScrollView with map (renders all items) <ScrollView> {items.map(item => ( <ItemComponent key={item.id} item={item} /> ))} </ScrollView>
// Good - FlatList (virtualizes items) <FlatList data={items} keyExtractor={item => item.id} renderItem={({ item }) => <ItemComponent item={item} />} />
Avoid Anonymous Functions in Render
// Bad - Creates new function on every render <FlatList data={items} renderItem={({ item }) => ( <TouchableOpacity onPress={() => console.log(item.id)}> <Text>{item.title}</Text> </TouchableOpacity> )} />
// Good - Memoized render function const renderItem = useCallback(({ item }: { item: Item }) => ( <ItemRow item={item} onPress={handleItemPress} /> ), [handleItemPress]);
<FlatList data={items} renderItem={renderItem} />
Optimize Bundle Size
Reduce JavaScript bundle size:
Analyze bundle
npx react-native-bundle-visualizer
Enable Hermes engine (app.json for Expo)
{ "expo": { "jsEngine": "hermes" } }
Enable Hermes (android/app/build.gradle for bare React Native)
project.ext.react = [ enableHermes: true ]
Use ProGuard for Android (android/app/build.gradle)
def enableProguardInReleaseBuilds = true
Code Splitting
Split code for faster initial load:
import React, { lazy, Suspense } from 'react'; import { View, ActivityIndicator } from 'react-native';
// Lazy load heavy components const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() { return ( <Suspense fallback={<ActivityIndicator />}> <HeavyComponent /> </Suspense> ); }
Common Patterns
Debounced Search
import React, { useState, useCallback, useEffect } from 'react'; import { TextInput, FlatList } from 'react-native';
function SearchableList({ data }: { data: Item[] }) { const [query, setQuery] = useState(''); const [debouncedQuery, setDebouncedQuery] = useState('');
// Debounce search useEffect(() => { const timer = setTimeout(() => { setDebouncedQuery(query); }, 300);
return () => clearTimeout(timer);
}, [query]);
const filteredData = useMemo(() => { if (!debouncedQuery) return data; return data.filter(item => item.title.toLowerCase().includes(debouncedQuery.toLowerCase()) ); }, [data, debouncedQuery]);
return ( <> <TextInput value={query} onChangeText={setQuery} placeholder="Search..." /> <FlatList data={filteredData} keyExtractor={item => item.id} renderItem={({ item }) => <ItemComponent item={item} />} /> </> ); }
Optimized Animations
Use react-native-reanimated for smooth animations:
import React from 'react'; import Animated, { useSharedValue, useAnimatedStyle, withSpring, } from 'react-native-reanimated'; import { Pressable } from 'react-native';
function AnimatedButton() { const scale = useSharedValue(1);
const animatedStyle = useAnimatedStyle(() => ({ transform: [{ scale: scale.value }], }));
const handlePressIn = () => { scale.value = withSpring(0.95); };
const handlePressOut = () => { scale.value = withSpring(1); };
return ( <Pressable onPressIn={handlePressIn} onPressOut={handlePressOut}> <Animated.View style={[styles.button, animatedStyle]}> <Text>Press Me</Text> </Animated.View> </Pressable> ); }
Pagination
Implement efficient pagination:
import React, { useState, useCallback } from 'react'; import { FlatList, ActivityIndicator } from 'react-native';
function PaginatedList() { const [data, setData] = useState<Item[]>([]); const [loading, setLoading] = useState(false); const [page, setPage] = useState(1);
const loadMore = useCallback(async () => { if (loading) return;
setLoading(true);
const newData = await fetchData(page);
setData(prev => [...prev, ...newData]);
setPage(p => p + 1);
setLoading(false);
}, [loading, page]);
const renderItem = useCallback( ({ item }: { item: Item }) => <ItemComponent item={item} />, [] );
const renderFooter = () => { if (!loading) return null; return <ActivityIndicator style={{ margin: 16 }} />; };
return ( <FlatList data={data} renderItem={renderItem} keyExtractor={item => item.id} onEndReached={loadMore} onEndReachedThreshold={0.5} ListFooterComponent={renderFooter} /> ); }
Memoized Selector
import React, { useMemo } from 'react'; import { View, Text } from 'react-native';
interface User { id: string; name: string; age: number; active: boolean; }
function UserStats({ users }: { users: User[] }) { const stats = useMemo(() => { return { total: users.length, active: users.filter(u => u.active).length, averageAge: users.reduce((sum, u) => sum + u.age, 0) / users.length, }; }, [users]);
return ( <View> <Text>Total: {stats.total}</Text> <Text>Active: {stats.active}</Text> <Text>Average Age: {stats.averageAge.toFixed(1)}</Text> </View> ); }
Image Preloading
import { Image } from 'react-native';
async function preloadImages(imageUrls: string[]) { const promises = imageUrls.map(url => Image.prefetch(url) );
await Promise.all(promises); }
// Usage useEffect(() => { preloadImages([ 'https://example.com/image1.jpg', 'https://example.com/image2.jpg', ]); }, []);
Anti-Patterns
Don't Use console.log in Production
// Bad - Logs slow down production console.log('User data:', user);
// Good - Remove or use DEV if (DEV) { console.log('User data:', user); }
Don't Inline Large Objects
// Bad - Creates new object on every render <Component style={{ width: 100, height: 100, backgroundColor: '#fff' }} config={{ option1: true, option2: false }} />
// Good - Use StyleSheet and constants const styles = StyleSheet.create({ component: { width: 100, height: 100, backgroundColor: '#fff' }, });
const config = { option1: true, option2: false };
<Component style={styles.component} config={config} />
Don't Forget to Clean Up
// Bad - Memory leak useEffect(() => { const timer = setInterval(() => { console.log('Tick'); }, 1000); }, []);
// Good - Clean up useEffect(() => { const timer = setInterval(() => { console.log('Tick'); }, 1000);
return () => clearInterval(timer); }, []);
Don't Use setState in Loops
// Bad - Multiple re-renders items.forEach(item => { setData(prev => [...prev, item]); });
// Good - Single update setData(prev => [...prev, ...items]);
Profiling Tools
React DevTools Profiler
import { Profiler } from 'react';
function onRenderCallback(
id: string,
phase: 'mount' | 'update',
actualDuration: number
) {
console.log(${id} (${phase}) took ${actualDuration}ms);
}
<Profiler id="MyComponent" onRender={onRenderCallback}> <MyComponent /> </Profiler>
Performance Monitor
import { PerformanceObserver, performance } from 'perf_hooks';
// Enable performance monitor
if (DEV) {
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
console.log(${entry.name}: ${entry.duration}ms);
});
});
observer.observe({ entryTypes: ['measure'] }); }
Related Skills
-
react-native-components: Building performant components
-
react-native-navigation: Optimizing navigation performance
-
react-native-native-modules: Native performance optimization