React Native Web - Performance
Performance optimization patterns for React Native Web, focusing on bundle size, rendering performance, and web-specific optimizations.
Key Concepts
Code Splitting
Use dynamic imports for lazy loading:
import React, { lazy, Suspense } from 'react'; import { View, ActivityIndicator } from 'react-native';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() { return ( <Suspense fallback={<ActivityIndicator />}> <HeavyComponent /> </Suspense> ); }
Memoization
Prevent unnecessary re-renders with React.memo and hooks:
import React, { memo, useMemo, useCallback } from 'react';
interface Props { items: Item[]; onItemPress: (id: string) => void; }
export const ItemList = memo(function ItemList({ items, onItemPress }: Props) { const sortedItems = useMemo( () => items.sort((a, b) => a.name.localeCompare(b.name)), [items] );
const handlePress = useCallback( (id: string) => { onItemPress(id); }, [onItemPress] );
return ( <View> {sortedItems.map(item => ( <Item key={item.id} item={item} onPress={handlePress} /> ))} </View> ); });
FlatList for Large Lists
Use FlatList for efficient rendering of long lists:
import { FlatList, View, Text } from 'react-native';
interface Item { id: string; title: string; }
function ItemsList({ items }: { items: Item[] }) { return ( <FlatList data={items} keyExtractor={item => item.id} renderItem={({ item }) => ( <View> <Text>{item.title}</Text> </View> )} initialNumToRender={10} maxToRenderPerBatch={10} windowSize={5} removeClippedSubviews /> ); }
Best Practices
Optimize Images
✅ Use optimized image formats and lazy loading:
import { Image } from 'react-native';
function OptimizedImage({ uri }: { uri: string }) { return ( <Image source={{ uri }} style={{ width: 200, height: 200 }} resizeMode="cover" // Web-specific: lazy loading {...(Platform.OS === 'web' && { loading: 'lazy', })} /> ); }
Avoid Inline Functions
✅ Use useCallback for event handlers:
import { useCallback } from 'react';
function Component({ onSave }: { onSave: (data: Data) => void }) { const [data, setData] = useState<Data>();
const handleSave = useCallback(() => { if (data) { onSave(data); } }, [data, onSave]);
return <Button onPress={handleSave} title="Save" />; }
Optimize StyleSheet
✅ Create StyleSheet outside component:
import { StyleSheet } from 'react-native';
// Good - created once const styles = StyleSheet.create({ container: { flex: 1, padding: 16, }, });
function Component() { return <View style={styles.container} />; }
// Bad - recreated on every render function BadComponent() { const styles = StyleSheet.create({ container: { flex: 1 }, }); return <View style={styles.container} />; }
Examples
Virtualized List with Optimization
import React, { useCallback, memo } from 'react'; import { FlatList, View, Text, StyleSheet } from 'react-native';
interface Item { id: string; title: string; description: string; }
interface ItemCardProps { item: Item; onPress: (id: string) => void; }
const ItemCard = memo(function ItemCard({ item, onPress }: ItemCardProps) { const handlePress = useCallback(() => { onPress(item.id); }, [item.id, onPress]);
return ( <Pressable onPress={handlePress}> <View style={styles.card}> <Text style={styles.title}>{item.title}</Text> <Text style={styles.description}>{item.description}</Text> </View> </Pressable> ); });
export function OptimizedList({ items, onItemPress }: { items: Item[]; onItemPress: (id: string) => void; }) { const renderItem = useCallback( ({ item }: { item: Item }) => ( <ItemCard item={item} onPress={onItemPress} /> ), [onItemPress] );
const keyExtractor = useCallback((item: Item) => item.id, []);
return ( <FlatList data={items} renderItem={renderItem} keyExtractor={keyExtractor} initialNumToRender={10} maxToRenderPerBatch={10} windowSize={5} removeClippedSubviews getItemLayout={(data, index) => ({ length: 80, offset: 80 * index, index, })} /> ); }
const styles = StyleSheet.create({ card: { padding: 16, backgroundColor: '#fff', marginBottom: 8, borderRadius: 8, }, title: { fontSize: 18, fontWeight: 'bold', marginBottom: 4, }, description: { fontSize: 14, color: '#666', }, });
Dynamic Imports
import React, { lazy, Suspense, useState } from 'react'; import { View, Button, ActivityIndicator } from 'react-native';
// Lazy load heavy components const Chart = lazy(() => import('./Chart')); const DataTable = lazy(() => import('./DataTable'));
export function Dashboard() { const [showChart, setShowChart] = useState(false);
return ( <View> <Button title="Show Chart" onPress={() => setShowChart(true)} />
{showChart && (
<Suspense fallback={<ActivityIndicator />}>
<Chart />
</Suspense>
)}
</View>
); }
Optimized Context
import React, { createContext, useContext, useMemo, ReactNode } from 'react';
interface User { id: string; name: string; }
interface UserContextValue { user: User | null; isLoading: boolean; }
const UserContext = createContext<UserContextValue | undefined>(undefined);
export function UserProvider({ user, isLoading, children, }: { user: User | null; isLoading: boolean; children: ReactNode; }) { // Memoize context value to prevent unnecessary re-renders const value = useMemo( () => ({ user, isLoading }), [user, isLoading] );
return <UserContext.Provider value={value}>{children}</UserContext.Provider>; }
export function useUser() { const context = useContext(UserContext); if (context === undefined) { throw new Error('useUser must be used within UserProvider'); } return context; }
Common Patterns
Debounced Input
import { useState, useEffect } from 'react'; import { TextInput } from 'react-native';
function SearchInput({ onSearch }: { onSearch: (query: string) => void }) { const [query, setQuery] = useState('');
useEffect(() => { const timer = setTimeout(() => { onSearch(query); }, 300);
return () => clearTimeout(timer);
}, [query, onSearch]);
return ( <TextInput value={query} onChangeText={setQuery} placeholder="Search..." /> ); }
Intersection Observer (Web)
import { useEffect, useRef, useState } from 'react'; import { View, Platform } from 'react-native';
function LazyLoadComponent({ children }: { children: React.ReactNode }) { const [isVisible, setIsVisible] = useState(false); const ref = useRef<View>(null);
useEffect(() => { if (Platform.OS !== 'web') { setIsVisible(true); return; }
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect();
}
},
{ threshold: 0.1 }
);
const element = ref.current as any;
if (element) {
observer.observe(element);
}
return () => observer.disconnect();
}, []);
return ( <View ref={ref}> {isVisible ? children : <View style={{ height: 200 }} />} </View> ); }
Bundle Size Optimization
// webpack.config.js or metro.config.js module.exports = { resolve: { alias: { // Use lightweight alternatives 'react-native$': 'react-native-web', 'lodash': 'lodash-es', }, }, optimization: { splitChunks: { chunks: 'all', cacheGroups: { vendor: { test: /[\/]node_modules[\/]/, priority: -10, }, }, }, }, };
Anti-Patterns
❌ Don't create StyleSheet inside render:
// Bad function Component() { const styles = StyleSheet.create({ container: { flex: 1 } }); return <View style={styles.container} />; }
// Good const styles = StyleSheet.create({ container: { flex: 1 } }); function Component() { return <View style={styles.container} />; }
❌ Don't use inline functions in FlatList:
// Bad <FlatList data={items} renderItem={({ item }) => <Item item={item} onPress={() => handlePress(item.id)} />} />
// Good const renderItem = useCallback(({ item }) => ( <Item item={item} onPress={handlePress} /> ), [handlePress]);
<FlatList data={items} renderItem={renderItem} />
❌ Don't import entire libraries:
// Bad import _ from 'lodash';
// Good import debounce from 'lodash/debounce';
❌ Don't render all items in long lists:
// Bad {items.map(item => <Item key={item.id} item={item} />)}
// Good <FlatList data={items} renderItem={({ item }) => <Item item={item} />} />
Related Skills
-
react-native-web-core: Core React Native Web concepts
-
react-native-web-styling: Optimized styling patterns
-
react-native-web-testing: Performance testing strategies