We’ve all been there: you build a feature that works perfectly on a high-end iPhone 15 Pro Max, but the moment you test it on a mid-range Android device, the animations stutter and the lists feel like they’re wading through molasses. Achieving native-like fluidity requires more than just writing clean code; it requires a deep understanding of how the React Native bridge and the JS engine actually work.
Over the last few years, I’ve spent countless hours profiling apps and hunting down memory leaks. Most performance issues stem from the same few culprits: unnecessary re-renders, oversized assets, and inefficient list rendering. In this guide, I’m sharing my top react native performance optimization tips that actually move the needle on frame rates.
1. Memoize Components with React.memo and useMemo
One of the most common performance killers is the unnecessary re-render. In React Native, every time a parent component updates, all its children re-render by default, even if their props haven’t changed.
I’ve found that wrapping expensive components in React.memo can drastically reduce the load on the JS thread. Combine this with useMemo for heavy calculations to ensure you aren’t recalculating data on every tick.
const HeavyComponent = React.memo(({ data }) => {
return <Text>{data.value}</Text>;
});
2. Switch from FlatList to FlashList
If you are dealing with long lists, FlatList can often struggle with “blank areas” during fast scrolling because it unmounts components too aggressively. This is where Shopify’s FlashList becomes a game-changer.
FlashList recycles views instead of destroying and recreating them, which significantly lowers the pressure on the memory manager. I wrote a detailed breakdown on shopify flashlist vs flatlist performance where I benchmarked the two; the difference in FPS is night and day for complex cells.
3. Optimize Image Assets and Caching
Large, unoptimized images are the fastest way to crash your app or cause “jank.” If you’re loading a 4K image into a 50×50 thumbnail, you’re wasting CPU and RAM.
I recommend using react-native-fast-image for aggressive caching and better priority handling. Additionally, always use WebP format over PNG/JPG where possible to reduce your react native app size and speed up load times.
4. Avoid Anonymous Functions in Render
Passing an arrow function directly into a prop—like onPress={() => doSomething()}—creates a new function instance on every single render. While it seems trivial, in a list of 100 items, this triggers 100 unnecessary re-renders of the child component.
Instead, define your handlers using useCallback:
const handlePress = useCallback(() => {
doSomething();
}, []);
return <Button onPress={handlePress} />;
5. Use the New Architecture (Fabric & TurboModules)
If you’re still on the old bridge architecture, you’re bottlenecked by asynchronous JSON serialization. The New Architecture replaces the bridge with JSI (JavaScript Interface), allowing JS to call native methods synchronously.
Enabling Fabric allows for synchronous UI updates, which eliminates the “white flash” often seen in complex screen transitions. It’s a steeper migration path, but the performance ceiling is significantly higher.
6. Move Heavy Logic to the Native Layer
JavaScript is fast, but it’s not C++ or Swift. If you’re doing heavy image processing, complex data sorting, or encryption, do not do it in the JS thread. I’ve seen apps freeze for 200ms just because they were sorting a large array on the main thread.
Write a custom Native Module to handle these tasks. By offloading this to a background native thread, your UI remains responsive while the heavy lifting happens in the background.
7. Use Native Driver for Animations
When using the Animated API, always set useNativeDriver: true whenever possible. This sends the animation configuration to the native side once, allowing the native OS to handle the animation frames without needing to talk back to the JS thread for every frame.
Animated.timing(this.state.fadeAnim, {
toValue: 1,
duration: 500,
useNativeDriver: true // Critical for performance
}).start();
8. Optimize Your JS Bundle Size
A bloated JS bundle leads to slower “Time to Interactive” (TTI). I suggest auditing your dependencies using tools like react-native-bundle-visualizer.
Often, I find that developers import entire libraries (like Lodash) when they only need one function. Use tree-shaking or import specific modules (e.g., import debounce from 'lodash/debounce') to keep the bundle lean. This ties directly into the strategies I mentioned for reducing the overall app footprint.
9. Reduce Overdraw and Complex View Hierarchies
Overdraw happens when the app draws the same pixel multiple times in a single frame. For example, having a background color on a container, and then another background color on a child view that covers the entire parent.
Keep your view hierarchy flat. Avoid nesting View components unnecessarily. Use StyleSheet.create to ensure styles are sent across the bridge only once rather than being recalculated inline.
10. Profile with Flipper and the Performance Monitor
You cannot optimize what you cannot measure. I always start my optimization phase by opening the Perf Monitor (Dev Menu > Show Perf Monitor).
If the UI thread is dropping below 60fps, you have a layout/native issue. If the JS thread is dropping, you have a logic/re-render issue. Flipper’s Flame Graph is invaluable here for identifying exactly which component is causing the bottleneck.
Common Mistakes to Avoid
- Over-memoizing: Don’t wrap every single component in
React.memo. Memoization has its own overhead; only use it for components that actually re-render frequently with the same props. - Using state for everything: Using
useStatefor values that don’t need to trigger a UI update (like timers or socket listeners) will kill your performance. UseuseRefinstead. - Ignoring Android: Optimizing only for iOS is a mistake. Android’s JS engine (Hermes) is great, but the hardware variance is much higher. Always test on a low-end Android device.
Measuring Your Success
To know if these react native performance optimization tips are working, I use a three-pronged approach:
- FPS Tracking: Aim for a consistent 60fps on both JS and UI threads during transitions.
- TTRC (Time to Render Content): Measure how long it takes for the first meaningful paint to appear after a screen transition.
- Memory Profiling: Use Xcode Instruments or Android Studio Profiler to ensure your memory usage doesn’t climb linearly (a sign of leaks).
Ready to take your app to the next level? Start by implementing the FlashList swap—it’s usually the quickest win for most production apps.