React Native is fast enough for nearly any consumer application — but only when you avoid the patterns that undermine its architecture. The JavaScript thread and the UI thread run separately, and the bridge between them is a bottleneck. Most performance problems in React Native trace back to putting too much work on the wrong thread, or re-rendering components that did not need to change. Here are the seven improvements that consistently deliver the largest gains.
1. Virtualise Every Long List
If you render a plain ScrollView with more than 20–30 items, you are rendering all of them at once regardless of whether they are visible. Replace it with FlatList or FlashList (from Shopify). FlashList in particular has a significantly more efficient recycling mechanism than FlatList and is worth adopting on any list that has noticeable scroll jank. Set a keyExtractor that returns a stable, unique value — avoid using array index as a key.
2. Move Animations to the Native Thread
Animations driven by the JavaScript thread drop frames the moment the JS thread is busy. Use the Animated API with useNativeDriver: true for any animation that only modifies opacity or transform properties. For more complex animations, Reanimated 3 runs animation worklets directly on the UI thread and eliminates the bridge overhead entirely. This alone can transform a jank-prone UI into one that feels native.
3. Memoize Aggressively but Selectively
Unnecessary re-renders are the most common source of perceived sluggishness. Wrap components that receive stable props in React.memo. Use useMemo for expensive computed values and useCallback for functions passed as props. However, do not memoize everything — the overhead of comparison can exceed the cost of re-rendering for simple components. Profile first with React DevTools Profiler to identify actual bottlenecks before adding memoisation.
4. Reduce JavaScript Bundle Size
Larger bundles mean longer startup times. Enable Hermes (it is the default from React Native 0.70 onwards, but verify it is active in your project). Use Metro's bundle analyser to identify large dependencies. Replace moment.js with date-fns or dayjs. Lazy-load screens that are not part of the critical startup path using React Navigation's lazy option on tab and stack navigators.
5. Optimise Image Loading
Images are frequently the largest contributor to memory pressure and layout jank. Use expo-image or react-native-fast-image instead of the built-in Image component — both implement memory and disk caching properly and avoid redundant network requests. Always specify explicit width and height to prevent layout shifts. Use WebP format where possible; it is 25–35% smaller than JPEG at equivalent quality.
6. Defer Non-Critical Work with InteractionManager
Heavy operations run during a navigation transition cause the transition animation to stutter. Wrap non-critical setup work in InteractionManager.runAfterInteractions() so it executes only after animations complete. This is particularly effective for analytics initialisation, non-urgent data fetching, and heavy component pre-computation.
7. Profile Before You Optimise
The most impactful performance improvement you can make is measuring before changing anything. Connect to the React Native Performance Monitor (⌘D on iOS simulator, ⌘M on Android) to see JS frame rate and UI frame rate in real time. Use Flipper with the React DevTools and Hermes profiler plugins to identify which functions are consuming the most time. Targeted fixes based on real data consistently outperform speculative optimisations applied without measurement.
Performance work is most effective when done incrementally. Pick the highest-impact item from this list, measure before and after, and move to the next. An hour of profiling saves days of guesswork.
