Performance Playbook: FlashList, Reanimated 3, and Profiling
FlatList to FlashList, Reanimated worklets, Hermes profiling, and the gotchas that don't show up until your app has 50,000 daily active users. A practical guide, not a theory textbook.
Performance work in React Native is a strange kind of engineering. The framework does a lot for you, so most of the time your app feels fast. Then you hit a list of 500 items, or a screen with 30 components re-rendering on every state change, and suddenly nothing works. After shipping apps to a few million users, here's the playbook I keep coming back to.
Replace FlatList with FlashList
This is the single biggest performance win you can get for free. FlashList, from Shopify, recycles component instances the way native lists do. Your 500-item list renders 10 items, not 500. The scroll is smooth, the memory usage drops by 80%, and you didn't have to change anything except the import.
The catch is that you need to provide a getItemType function if your list has multiple item layouts. Without it, FlashList falls back to recycling items of different shapes, which can cause visual bugs. Two minutes of work for a major win.
The other catch is estimatedItemSize. FlashList needs to know the height of each item (or at least a reasonable estimate) to calculate the scroll position. If you have variable-height items, measure them once and cache the heights. Don't guess.
Use Reanimated 3 for animations
The React Native Animated API runs animations on the JavaScript thread. If the JS thread is busy (because you're navigating screens, or parsing a big JSON response, or running a Redux action chain), your animation stutters. Users see jank, and they assume the app is broken.
Reanimated 3 runs animations on the UI thread, using worklets — JavaScript functions that get compiled to native code at build time. The animation never touches the JS thread. It's the difference between 60fps and "drops to 30fps when I scroll fast."
The mental model is different — you use useSharedValue instead of Animated.Value, and you write worklets that run on the UI thread. But once it clicks, you'll never go back. The combination of useAnimatedStyle and withSpring replaces 80% of the animation code I used to write.
Profile before you optimize
The biggest mistake I see is teams optimizing without measuring. They'll spend a week reworking a screen to use memo and useCallback, only to find out the actual problem was a list re-rendering 200 items at once.
Use the React DevTools Profiler to see which components re-render and why. Use the Performance tab in Chrome (or Safari, for iOS Simulator) to see where the time is actually going. Most of the time, you'll find that one component is the bottleneck, and you can fix it in 10 minutes.
For native-side performance, use the iOS and Android profilers directly. Xcode Instruments and Android Studio Profiler can show you where the main thread is spending its time, which JS bridge calls are blocking, and where memory is leaking.
Hermes is the default for a reason
Hermes, the JavaScript engine optimized for React Native, used to be opt-in. Now it's the default, and for good reason. Apps start faster, use less memory, and run JavaScript faster — especially on lower-end Android devices.
If you're not on Hermes yet, enable it. The migration is usually a one-line change in your build config, and the wins are immediate.
The long tail of small things
Once you've handled the big wins, there's a long tail of small optimizations that add up.
Image caching with FastImage. Avoiding inline styles (they create new objects every render). Using React.memo on components that don't need to re-render. Replacing console.log with a no-op in production (the console is surprisingly expensive). Using getItemLayout on lists when you know the item size.
None of these are dramatic on their own. Together, they add up to an app that feels noticeably faster — especially on mid-range Android devices that are the majority of your user base.
When to stop
There's a temptation to keep optimizing forever. Resist it. Once your app runs at 60fps on a 3-year-old Android phone, you've probably done enough. Spend the rest of your time on features users actually want.