Skip to main content

Performance Optimization

Performance is crucial for mobile applications. This guide covers optimization techniques for React Native apps, including bundle optimization, memory management, and rendering performance.

Bundle Optimization

Code Splitting with Dynamic Imports

// Lazy load screens to reduce initial bundle size
import { lazy, Suspense } from "react"

const ProfileScreen = lazy(() => import("@/screens/ProfileScreen"))
const SettingsScreen = lazy(() => import("@/screens/SettingsScreen"))

function AppNavigator() {
return (
<Stack.Navigator>
<Stack.Screen
name="Profile"
component={({ navigation, route }) => (
<Suspense fallback={<LoadingSpinner />}>
<ProfileScreen navigation={navigation} route={route} />
</Suspense>
)}
/>
</Stack.Navigator>
)
}

Bundle Analysis

// metro.config.js
const { getDefaultConfig } = require("expo/metro-config")

const config = getDefaultConfig(__dirname)

// Enable bundle splitting
config.serializer.createModuleIdFactory = () => {
return (path) => {
// Create deterministic module IDs
return require("crypto").createHash("sha1").update(path).digest("hex").substr(0, 8)
}
}

// Add bundle analyzer
if (process.env.ANALYZE_BUNDLE) {
config.serializer.customSerializer = require("@expo/metro-config/build/serializer/customSerializer").default({
analyzer: true,
})
}

module.exports = config

Tree Shaking and Dead Code Elimination

// Use specific imports instead of barrel exports
// ❌ Imports entire library
import * as _ from "lodash"

// ✅ Import only what you need
import debounce from "lodash/debounce"
import throttle from "lodash/throttle"

// ❌ Barrel export imports everything
import { Button, Input, Modal } from "@/components"

// ✅ Direct imports
import { Button } from "@/components/Button"
import { Input } from "@/components/Input"
import { Modal } from "@/components/Modal"

Memory Management

Component Cleanup

function DataFetchingComponent() {
const [data, setData] = useState([])
const abortControllerRef = useRef<AbortController>()

useEffect(() => {
abortControllerRef.current = new AbortController()

const fetchData = async () => {
try {
const response = await fetch("/api/data", {
signal: abortControllerRef.current.signal,
})
const result = await response.json()
setData(result)
} catch (error) {
if (error.name !== "AbortError") {
console.error("Fetch error:", error)
}
}
}

fetchData()

return () => {
// Cancel ongoing requests
abortControllerRef.current?.abort()
}
}, [])

return <DataList data={data} />
}

Memory Leak Prevention

function TimerComponent() {
const intervalRef = useRef<NodeJS.Timeout>()
const timeoutRef = useRef<NodeJS.Timeout>()

useEffect(() => {
// Set up timers
intervalRef.current = setInterval(() => {
console.log("Interval tick")
}, 1000)

timeoutRef.current = setTimeout(() => {
console.log("Timeout executed")
}, 5000)

return () => {
// Clean up timers
if (intervalRef.current) {
clearInterval(intervalRef.current)
}
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
}
}, [])

return <View />
}

// Event listener cleanup
function EventListenerComponent() {
useEffect(() => {
const handleAppStateChange = (nextAppState: AppStateStatus) => {
console.log("App state changed:", nextAppState)
}

const subscription = AppState.addEventListener("change", handleAppStateChange)

return () => {
subscription?.remove()
}
}, [])

return <View />
}

Image Memory Optimization

import { Image } from "expo-image"

function OptimizedImageList({ images }: { images: string[] }) {
return (
<FlatList
data={images}
renderItem={({ item }) => (
<Image
source={{ uri: item }}
style={styles.image}
contentFit="cover"
cachePolicy="memory-disk"
// Reduce memory usage
recyclingKey={item}
// Use placeholder for better UX
placeholder="https://via.placeholder.com/300x200"
transition={200}
/>
)}
// Remove images from memory when not visible
removeClippedSubviews={true}
keyExtractor={(item, index) => `${item}-${index}`}
/>
)
}

Rendering Performance

List Optimization

interface Item {
id: string
title: string
description: string
}

function OptimizedList({ data }: { data: Item[] }) {
// Memoize render item to prevent unnecessary re-renders
const renderItem = useCallback(({ item }: { item: Item }) => <ListItem item={item} />, [])

// Memoize key extractor
const keyExtractor = useCallback((item: Item) => item.id, [])

// Memoize item layout for better scrolling performance
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
maxToRenderPerBatch={10}
updateCellsBatchingPeriod={50}
initialNumToRender={10}
windowSize={10}
removeClippedSubviews={true}
// Avoid unnecessary re-renders
extraData={data.length}
/>
)
}

// Memoized list item component
const ListItem = memo(({ item }: { item: Item }) => {
return (
<View style={styles.item}>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.description}>{item.description}</Text>
</View>
)
})

Component Memoization

// Use React.memo for components that receive stable props
const ExpensiveComponent = memo(
({ data, onPress }: { data: ComplexData; onPress: () => void }) => {
// Expensive computations
const processedData = useMemo(() => {
return data.items.map((item) => ({
...item,
computed: expensiveCalculation(item),
}))
}, [data.items])

return (
<View>
{processedData.map((item) => (
<View key={item.id}>
<Text>{item.computed}</Text>
</View>
))}
<Button onPress={onPress} title="Action" />
</View>
)
},
(prevProps, nextProps) => {
// Custom comparison function
return prevProps.data.version === nextProps.data.version && prevProps.onPress === nextProps.onPress
}
)

// Use useMemo for expensive calculations
function DataProcessor({ rawData }: { rawData: any[] }) {
const processedData = useMemo(() => {
return rawData
.filter((item) => item.active)
.map((item) => ({
...item,
formatted: formatComplexData(item),
}))
.sort((a, b) => a.priority - b.priority)
}, [rawData])

return <DataDisplay data={processedData} />
}

// Use useCallback for stable function references
function ParentComponent() {
const [count, setCount] = useState(0)
const [users, setUsers] = useState([])

// Memoize callbacks to prevent child re-renders
const handleIncrement = useCallback(() => {
setCount((prev) => prev + 1)
}, [])

const handleUserUpdate = useCallback((userId: string, data: any) => {
setUsers((prev) => prev.map((user) => (user.id === userId ? { ...user, ...data } : user)))
}, [])

return (
<View>
<Counter count={count} onIncrement={handleIncrement} />
<UserList users={users} onUserUpdate={handleUserUpdate} />
</View>
)
}

Animation Performance

Using Native Driver

import { Animated } from "react-native"

function AnimatedComponent() {
const fadeAnim = useRef(new Animated.Value(0)).current
const scaleAnim = useRef(new Animated.Value(1)).current

const fadeIn = () => {
Animated.timing(fadeAnim, {
toValue: 1,
duration: 300,
useNativeDriver: true, // Run on native thread
}).start()
}

const scaleUp = () => {
Animated.spring(scaleAnim, {
toValue: 1.2,
useNativeDriver: true, // Run on native thread
}).start()
}

return (
<Animated.View
style={{
opacity: fadeAnim,
transform: [{ scale: scaleAnim }],
}}
>
<Text>Animated Content</Text>
</Animated.View>
)
}

React Native Reanimated

import Animated, { useSharedValue, withSpring, useAnimatedStyle, runOnJS } from "react-native-reanimated"

function ReanimatedComponent() {
const offset = useSharedValue(0)
const opacity = useSharedValue(1)

const animatedStyles = useAnimatedStyle(() => {
return {
transform: [{ translateX: offset.value }],
opacity: opacity.value,
}
})

const moveAndFade = () => {
offset.value = withSpring(100)
opacity.value = withSpring(0.5, {}, () => {
// Run JavaScript code after animation
runOnJS(() => {
console.log("Animation completed")
})()
})
}

return (
<View>
<Animated.View style={[styles.box, animatedStyles]} />
<Button title="Animate" onPress={moveAndFade} />
</View>
)
}

State Management Performance

Efficient Context Usage

// Split contexts to prevent unnecessary re-renders
const UserContext = createContext<User | null>(null)
const ThemeContext = createContext<Theme>(defaultTheme)

// Instead of one large context
const AppContext = createContext<{
user: User | null
theme: Theme
// ... other values
}>(defaultValue)

// Use multiple providers
function App() {
return (
<UserProvider>
<ThemeProvider>
<AppContent />
</ThemeProvider>
</UserProvider>
)
}

// Memoize context values
function UserProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null)

const contextValue = useMemo(
() => ({
user,
setUser,
}),
[user]
)

return <UserContext.Provider value={contextValue}>{children}</UserContext.Provider>
}

Optimized Selectors

// Use selectors to prevent unnecessary re-renders
const useUserName = () => {
const user = useContext(UserContext)
return useMemo(() => user?.name, [user?.name])
}

const useUserEmail = () => {
const user = useContext(UserContext)
return useMemo(() => user?.email, [user?.email])
}

// Component only re-renders when name changes
function UserGreeting() {
const userName = useUserName()
return <Text>Hello, {userName}!</Text>
}

Network Performance

Request Optimization

// Debounce search requests
function SearchComponent() {
const [query, setQuery] = useState("")
const [results, setResults] = useState([])

const debouncedSearch = useMemo(
() =>
debounce(async (searchQuery: string) => {
if (searchQuery.length > 2) {
const results = await searchAPI(searchQuery)
setResults(results)
}
}, 300),
[]
)

useEffect(() => {
debouncedSearch(query)

return () => {
debouncedSearch.cancel()
}
}, [query, debouncedSearch])

return (
<View>
<TextInput value={query} onChangeText={setQuery} placeholder="Search..." />
<SearchResults results={results} />
</View>
)
}

// Request deduplication
class RequestCache {
private cache = new Map<string, Promise<any>>()

async get<T>(key: string, fetcher: () => Promise<T>): Promise<T> {
if (this.cache.has(key)) {
return this.cache.get(key)
}

const promise = fetcher().finally(() => {
this.cache.delete(key)
})

this.cache.set(key, promise)
return promise
}
}

const requestCache = new RequestCache()

export async function fetchUser(id: string) {
return requestCache.get(`user-${id}`, () => apiClient.get(`/users/${id}`))
}

Caching Strategies

import AsyncStorage from "@react-native-async-storage/async-storage"

class CacheManager {
private memoryCache = new Map<string, { data: any; expiry: number }>()

async get<T>(key: string): Promise<T | null> {
// Check memory cache first
const memoryCached = this.memoryCache.get(key)
if (memoryCached && Date.now() < memoryCached.expiry) {
return memoryCached.data
}

// Check persistent storage
try {
const stored = await AsyncStorage.getItem(key)
if (stored) {
const parsed = JSON.parse(stored)
if (Date.now() < parsed.expiry) {
// Add to memory cache
this.memoryCache.set(key, parsed)
return parsed.data
}
}
} catch (error) {
console.error("Cache read error:", error)
}

return null
}

async set<T>(key: string, data: T, ttlMinutes: number = 60): Promise<void> {
const expiry = Date.now() + ttlMinutes * 60 * 1000
const cacheItem = { data, expiry }

// Store in memory
this.memoryCache.set(key, cacheItem)

// Store persistently
try {
await AsyncStorage.setItem(key, JSON.stringify(cacheItem))
} catch (error) {
console.error("Cache write error:", error)
}
}

async clear(): Promise<void> {
this.memoryCache.clear()
try {
await AsyncStorage.clear()
} catch (error) {
console.error("Cache clear error:", error)
}
}
}

export const cacheManager = new CacheManager()

Performance Monitoring

Custom Performance Hooks

function usePerformanceMonitor(componentName: string) {
const renderCountRef = useRef(0)
const mountTimeRef = useRef(Date.now())

useEffect(() => {
renderCountRef.current += 1
})

useEffect(() => {
const mountTime = Date.now() - mountTimeRef.current
console.log(`${componentName} mounted in ${mountTime}ms`)

return () => {
console.log(`${componentName} rendered ${renderCountRef.current} times`)
}
}, [componentName])

return {
renderCount: renderCountRef.current,
logPerformance: (operation: string, startTime: number) => {
console.log(`${componentName} ${operation}: ${Date.now() - startTime}ms`)
},
}
}

// Usage
function MyComponent() {
const { logPerformance } = usePerformanceMonitor("MyComponent")

const handleExpensiveOperation = async () => {
const startTime = Date.now()
await expensiveOperation()
logPerformance("expensive operation", startTime)
}

return <View />
}

Bundle Size Analysis

# Add to package.json scripts
{
"scripts": {
"analyze:bundle": "ANALYZE_BUNDLE=true expo export",
"bundle:size": "react-native bundle --platform android --dev false --entry-file index.js --bundle-output android-bundle.js && ls -lh android-bundle.js"
}
}

Platform-Specific Optimizations

iOS Optimizations

import { Platform } from "react-native"

// Use iOS-specific optimizations
const styles = StyleSheet.create({
shadowOptimized: Platform.select({
ios: {
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 3.84,
// Use shadowPath for better performance
shadowPath: "M0,0 L100,0 L100,50 L0,50 Z",
},
android: {
elevation: 5,
},
}),
})

Android Optimizations

// Enable ProGuard for production builds
// android/app/build.gradle
android {
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}

// Use Android-specific optimizations
const AndroidOptimizedComponent = () => {
return (
<View
style={{
// Use hardware acceleration
renderToHardwareTextureAndroid: true,
// Optimize for specific use cases
needsOffscreenAlphaCompositing: false,
}}
>
<Text>Optimized for Android</Text>
</View>
);
};

Next Steps

Continue with these related topics: