React Native Performance
Performance optimization strategies for fast, responsive banking applications.
Performance in React Native applications stems from understanding the bridge architecture (or Fabric in newer versions). JavaScript and native code run in separate threads, communicating asynchronously through a bridge. Every time you update state that affects the UI, React calculates a diff, serializes it to JSON, sends it across the bridge, and native code updates the UI. This bridge communication is a performance bottleneck.
The key performance principle: minimize bridge traffic. Each bridge crossing has overhead (serialization, queueing, deserialization). When scrolling a list of 1000 transactions, you don't want to render all 1000 items because that would send 1000 separate UI update messages across the bridge. FlatList virtualizes the list, rendering only visible items (typically 10-20) and reusing components as you scroll. This reduces bridge traffic from thousands of messages to dozens.
Another critical optimization: separate JavaScript work from UI work. Heavy computations in your component's render method block the JavaScript thread, causing frame drops and janky scrolling. The target is 60 FPS (frames per second), meaning each frame has a 16.67ms budget. If calculating transaction totals takes 50ms, you've blown through 3 frames, causing visible stuttering. Use memoization (useMemo, useCallback, React.memo) to cache expensive calculations and prevent recalculating on every render.
Hermes is React Native's JavaScript engine optimized for mobile. Compared to JavaScriptCore, Hermes has faster startup (bytecode vs parsing JavaScript), lower memory usage (optimized garbage collector), and smaller bundle size (bytecode is more compact). For banking apps where initial load time impacts perceived quality, Hermes typically reduces startup time by 30-50%. See React Performance for React-specific optimization patterns that apply to React Native.
Performance Optimization Strategy
FlatList Optimization
FlatList is React Native's solution for efficiently rendering large lists. Unlike ScrollView which renders all children upfront (causing performance issues with 1000+ items), FlatList implements virtualization: rendering only items currently visible on screen plus a small buffer above/below for smooth scrolling.
The virtualization process: FlatList calculates which items are visible based on scroll position, renders those items, and recycles components as you scroll. When you scroll down and item 1 disappears off screen, FlatList unmounts it and reuses its component instance to render item 21 now entering the viewport. This recycling drastically reduces memory usage and bridge traffic.
Performance optimization techniques:
keyExtractor: Provides unique keys so React can track which items are which during recycling. Without stable keys, FlatList might confuse items, causing unnecessary re-renders or wrong data displayed.getItemLayout: Tells FlatList each item's exact height upfront, allowing instant scroll-to calculations. Without this, FlatList must render each item to measure it, making large scrolls sluggish.removeClippedSubviews: Unmounts off-screen items from the native view hierarchy, freeing native memory. Critical for lists with complex items (images, nested views).initialNumToRender: Balances quick initial display (few items) vs smooth initial scroll (more items). For banking apps, 15 items fills most screens while avoiding long initial render.windowSize: Controls how many screens of items to keep mounted. Larger windows reduce blank flashes when scrolling fast, but increase memory usage. 21 screens (10 above, current, 10 below) works well for most lists.
See React Native UI for component memoization patterns and React Performance for React rendering optimization fundamentals.
// screens/payments/PaymentListScreen.tsx
import React, { useCallback, useMemo } from 'react';
import { FlatList, View } from 'react-native';
import PaymentCard from '../../components/PaymentCard';
import { usePayments } from '../../hooks/usePayments';
import type { Payment } from '../../types/payment';
export default function PaymentListScreen() {
const { data: payments, isLoading } = usePayments();
// Memoize keyExtractor to prevent recreating function on every render
const keyExtractor = useCallback((item: Payment) => item.id, []);
// Memoize renderItem to prevent unnecessary re-renders of list items
const renderItem = useCallback(
({ item }: { item: Payment }) => (
<PaymentCard payment={item} />
),
[]
);
// Memoize ItemSeparator to avoid creating new component instances
const ItemSeparator = useCallback(
() => <View style={{ height: 8 }} />,
[]
);
// Memoize getItemLayout for instant scroll-to-index calculations
// Requires all items to have fixed height (120dp in this case)
const getItemLayout = useCallback(
(_data: Payment[] | null | undefined, index: number) => ({
length: 120, // Fixed item height
offset: 120 * index, // Calculate exact position
index,
}),
[]
);
if (isLoading) return <LoadingSpinner />;
return (
<FlatList
data={payments}
keyExtractor={keyExtractor}
renderItem={renderItem}
ItemSeparatorComponent={ItemSeparator}
getItemLayout={getItemLayout}
// Performance optimizations
removeClippedSubviews={true} // Unmount off-screen items
maxToRenderPerBatch={10} // Render 10 items per batch
updateCellsBatchingPeriod={50} // Update every 50ms
initialNumToRender={15} // Render 15 items initially
windowSize={21} // Maintain 21 screens of items
/>
);
}
Memoization
// components/PaymentCard.tsx
import React, { memo } from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
import { formatCurrency } from '../utils/formatters';
import type { Payment } from '../types/payment';
interface PaymentCardProps {
payment: Payment;
onPress?: (id: string) => void;
}
/**
* Memoized PaymentCard prevents re-render when props haven't changed
*/
const PaymentCard = memo<PaymentCardProps>(
({ payment, onPress }) => {
return (
<TouchableOpacity
onPress={() => onPress?.(payment.id)}
>
<Text>{payment.recipientName}</Text>
<Text>{formatCurrency(payment.amount)}</Text>
</TouchableOpacity>
);
},
// Custom comparison function
(prevProps, nextProps) => {
return (
prevProps.payment.id === nextProps.payment.id &&
prevProps.payment.status === nextProps.payment.status
);
}
);
export default PaymentCard;
Image Optimization
// Use react-native-fast-image for better performance
import FastImage from 'react-native-fast-image';
export default function Avatar({ uri }: { uri: string }) {
return (
<FastImage
source={{
uri,
priority: FastImage.priority.normal,
cache: FastImage.cacheControl.immutable,
}}
style={{ width: 50, height: 50, borderRadius: 25 }}
resizeMode={FastImage.resizeMode.cover}
/>
);
}
Bundle Size Optimization
// metro.config.js
const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');
const config = {
transformer: {
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: true, // Lazy load modules
},
}),
minifierConfig: {
compress: {
drop_console: true, // Remove console.log in production
},
},
},
};
module.exports = mergeConfig(getDefaultConfig(__dirname), config);
Code Splitting
// Lazy load heavy screens
import React, { lazy, Suspense } from 'react';
const TransactionHistoryScreen = lazy(() =>
import('../screens/TransactionHistoryScreen')
);
export default function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<TransactionHistoryScreen />
</Suspense>
);
}
Hermes Engine
// android/app/build.gradle
project.ext.react = [
enableHermes: true, // Enable Hermes for better performance
]
// Hermes benefits:
// - Faster app startup
// - Reduced memory usage
// - Smaller APK size
// - Bytecode caching
Common Mistakes
Using ScrollView for Long Lists
// BAD: ScrollView renders all items
<ScrollView>
{payments.map(payment => (
<PaymentCard key={payment.id} payment={payment} />
))}
</ScrollView>
// GOOD: FlatList virtualizes items
<FlatList
data={payments}
renderItem={({ item }) => <PaymentCard payment={item} />}
keyExtractor={(item) => item.id}
/>
Not Memoizing Callbacks
// BAD: New function on every render
<FlatList
data={payments}
renderItem={({ item }) => <PaymentCard payment={item} />}
/>
// GOOD: Memoized renderItem
const renderItem = useCallback(
({ item }) => <PaymentCard payment={item} />,
[]
);
<FlatList data={payments} renderItem={renderItem} />
Code Review Checklist
List Performance
- FlatList used for all lists >10 items
- keyExtractor memoized
- renderItem memoized
- getItemLayout implemented for fixed-height items
- removeClippedSubviews enabled
- windowSize optimized
Component Optimization
- React.memo used for expensive components
- useCallback for event handlers
- useMemo for expensive calculations
- Prop drilling avoided (use Context/Zustand)
Bundle Size
- Hermes enabled for Android and iOS
- inlineRequires enabled in Metro config
- Lazy loading for heavy screens
- drop_console enabled for production
- Unused dependencies removed
Images
- FastImage used instead of Image
- Image caching enabled
- Proper resizeMode set
- Image dimensions specified
- WebP format for Android
Further Reading
React Native Framework Guidelines
- React Native Overview - Hermes and build configuration
- React Native UI - Component rendering performance
- React Native Data - Data layer performance
Performance Guidelines
- React Performance - React optimization patterns
- Performance Overview - Performance strategy
- Performance Optimization - Cross-platform optimization
- Performance Testing - Performance benchmarking
Mobile Guidelines
- Mobile Overview - Mobile performance patterns
- Mobile Performance - Cross-platform mobile optimization
External Resources
Summary
Key Takeaways
- FlatList over ScrollView - Virtualization for long lists
- Memoization - useCallback, useMemo, React.memo
- Hermes engine - Faster startup, smaller bundle
- Image optimization - FastImage with caching
- Bundle optimization - inlineRequires, code splitting
- Remove clipped subviews - Unmount off-screen items
- getItemLayout - Instant scrolling for fixed-height items
- Lazy loading - Split large components
- Profile regularly - Use Flipper and React DevTools
- Drop console.log - Remove in production builds
Next Steps: Set up Flipper for performance profiling and monitor your app's performance in production.