Performance Is a Feature
Users do not consciously think about performance — they just feel it. A 300ms delay when tapping a button, a janky scroll through a product list, a 5-second app launch — these create a subconscious impression that your app is low quality. Performance optimization is not premature optimization; it is a core feature.
FlatList Optimization
FlatList is the workhorse component for rendering lists in React Native. Poorly configured FlatLists are the #1 source of janky scrolling.
Key Props for Performance
`getItemLayout`: If all items have the same height, provide this function. It eliminates the need for React Native to measure each item dynamically, which is expensive for long lists. `removeClippedSubviews`: Set to `true` for long lists. Items scrolled off-screen are detached from the native view hierarchy, reducing memory usage. `maxToRenderPerBatch`: Controls how many items are rendered per batch during scroll. Default is 10. Reduce to 5 for complex items, increase to 20 for simple items. `windowSize`: Determines how many screens worth of items are rendered around the current viewport. Default is 21 (10 screens above, 10 below, 1 current). Reduce to 5 for memory-constrained scenarios. `initialNumToRender`: Number of items rendered in the initial batch. Set to the number of items visible on screen without scrolling. Item Component Optimization
Each item in a FlatList should be wrapped in React.memo to prevent re-renders when other items change. Extract item components to separate files to ensure memoization works correctly:
Avoid creating new objects or arrays in item props (these break memoization) Extract event handlers with `useCallback` Use `keyExtractor` with stable, unique keys — never use array index as the key for dynamic lists React.memo, useMemo, and useCallback
These memoization tools prevent unnecessary work:
`React.memo`: Wraps a component to skip re-rendering when props have not changed. Use on list items, cards, and any component that renders frequently. `useMemo`: Memoizes expensive computations. Use for filtering, sorting, or transforming large data arrays. `useCallback`: Memoizes function references. Essential for functions passed as props to memoized child components. When NOT to Use Memoization
Memoization has a cost — it uses memory and adds comparison overhead. Do not memoize:
Components that always receive new props (memoization comparison is wasted) Cheap computations (the comparison cost exceeds the computation cost) Components that render infrequently (no performance benefit) Hermes Engine
Hermes is Meta's JavaScript engine optimized for React Native. It should be enabled for every React Native project:
Faster startup: Hermes uses ahead-of-time compilation to bytecode. The JavaScript engine does not need to parse and compile your JavaScript at runtime. Startup times improve by 30-50%. Lower memory usage: Hermes is designed for memory-constrained mobile devices. It uses less memory than JavaScriptCore (the default iOS engine). Smaller binary size: The bytecode format is more compact than raw JavaScript. In React Native 0.70+, Hermes is the default engine. For older projects, enable it in android/app/build.gradle and ios/Podfile.
Native Modules for Performance-Critical Code
When JavaScript performance is insufficient — image processing, cryptography, complex animations, or heavy computation — write native modules:
Turbo Modules: (New Architecture): Type-safe, lazily loaded native modules with synchronous access. The recommended approach for React Native 0.71+. Legacy Native Modules: The older bridge-based approach. Still works but has async-only communication overhead. Common candidates for native modules:
Image manipulation (cropping, filtering, compression) Complex mathematical computations Database operations with large datasets Bundle Size Reduction
A smaller bundle means faster downloads, faster updates, and faster startup:
Analyze your bundle: Use `react-native-bundle-visualizer` to see what is in your bundle. Common culprits: moment.js (use date-fns instead), lodash (import individual functions), unused dependencies. Tree shaking: Ensure your bundler (Metro) eliminates dead code. Use ES module imports (`import { map } from "lodash-es"`) instead of CommonJS (`const _ = require("lodash")`). Lazy loading: Use `React.lazy` and dynamic imports for screens that users access infrequently. The settings screen does not need to be in the initial bundle. Asset optimization: Compress images, use WebP format, and avoid bundling large assets — download them on demand instead. Profiling Tools
React DevTools Profiler
Identifies components that re-render unnecessarily. Look for components that re-render without visual changes — these are candidates for React.memo.
Flipper
Meta's debugging tool for React Native provides:
Performance monitor: Real-time FPS, CPU, and memory graphs React DevTools integration: Inspect component tree and props Network inspector: Monitor API calls and responses Database inspector: Browse SQLite databases on-device Xcode Instruments and Android Studio Profiler
For native-level profiling:
Xcode Instruments: Time Profiler for CPU bottlenecks, Allocations for memory leaks, Core Animation for rendering performance Android Studio Profiler: CPU, memory, network, and energy profiling with method-level granularity Performance Budgets
Set concrete performance targets and enforce them:
App startup: Under 2 seconds on mid-range devices Screen transitions: Under 300ms FlatList scroll: Consistent 60 FPS Bundle size: Under 15MB for the initial download Memory usage: Under 200MB during typical usage Add performance tests to your CI pipeline. If a commit causes startup time to exceed the budget, the build fails. This prevents gradual performance degradation that goes unnoticed until users complain.
Performance optimization is iterative — measure, identify the bottleneck, fix it, and measure again. Need help profiling your React Native app? Talk to us.