React Native Performance
Users judge mobile apps by feel: 60 (or 120) frames per second scrolling, instant responses to taps, and quick launches. A React Native app has two main places where performance goes wrong. The JavaScript thread gets busy (too many re-renders, heavy computation, large JSON parsing), which delays responses to touches. The UI thread has too much to draw (huge lists, unoptimized images, expensive layouts and animations).
Most issues have well-known fixes: virtualized lists (FlashList or tuned FlatList), avoiding unnecessary re-renders, running animations on the UI thread with Reanimated, optimizing images, and trimming startup work. As always, measure before optimizing, on real devices, in release builds.
TL;DR
- Profile release builds on real (ideally low-end Android) devices; debug builds are much slower.
- Keep the JS thread free: avoid heavy synchronous work, excessive re-renders, and large state updates during interactions.
- Use FlashList (or a tuned FlatList) for long lists, never
ScrollViewwith.map()over hundreds of items. - Prevent unnecessary re-renders with memoization (or the React Compiler), stable callbacks, and colocated state.
- Run animations and gestures on the UI thread with Reanimated and Gesture Handler.
- Optimize images (size, caching with expo-image), and startup (Hermes, lazy loading, deferring non-critical work).
Quick Example
A performant product list:
Core Concepts
Measuring
- Perf Monitor (dev menu) shows JS and UI thread frame rates.
- React Native DevTools gives a React Profiler (which components render and why), a JS CPU profiler, and memory tools.
- Platform tools: Xcode Instruments (Time Profiler, Allocations), Android Studio Profiler, and Perfetto traces.
- Flashlight and Reassure automate performance measurement and regression testing.
- Production monitoring: startup time, slow and frozen frames, ANRs (Android "app not responding"), via Sentry, Firebase Performance, or Datadog.
Always measure release builds: development mode adds checks and runs slower.
JS Thread vs UI Thread
- If the UI thread drops frames, the problem is rendering: too many views, overdraw, large images, or complex layout.
- If the JS thread drops frames, JavaScript work is too heavy: re-renders, data processing, or synchronous logic during scrolls and gestures.
With the New Architecture, communication is cheaper, but a busy JS thread still delays responses to user input.
Lists
Long lists are the most common performance problem.
- FlashList (Shopify) recycles item views instead of unmounting and remounting them, giving much better scroll performance and memory use, especially on Android. Newer versions measure items automatically.
- FlatList virtualizes by rendering a window of items. Tune
windowSize,initialNumToRender,maxToRenderPerBatch, andremoveClippedSubviews, and usegetItemLayoutfor fixed-height rows. - Never render large collections with
ScrollViewplus.map(), since everything mounts at once. - Keep item components light and memoized, pass stable props, and provide good
keyExtractors.
Avoiding Unnecessary Re-renders
The same principles as React performance:
- Colocate state: a text input's state shouldn't re-render the whole screen.
- Memoize list items and expensive children (
memo,useCallback,useMemo), or enable the React Compiler, which automates it. - Split contexts, and use selector-based stores (Zustand, Legend State, Redux Toolkit) for frequently changing global state.
- Avoid creating new objects and functions in props passed to memoized children.
Animations and Gestures
Animations driven from JavaScript stutter whenever the JS thread is busy. Reanimated runs animation logic as worklets on the UI thread via JSI, and react-native-gesture-handler processes gestures natively. Combined, they keep interactions at full frame rate even while JS is working. Prefer animating transform and opacity, which are cheap, over layout properties.
Images
- Serve appropriately sized images (thumbnails for lists) from a CDN with resizing. See CDN.
- Use expo-image (or FastImage-style libraries) for disk and memory caching, placeholders (blurhash), and efficient formats (WebP, AVIF).
- Oversized images cost decoding time and memory, and they're a common cause of out-of-memory crashes on Android.
Startup Time
- Hermes precompiles bytecode for faster startup (the default).
- Lazy-load screens and heavy modules; TurboModules load native modules on demand.
- Defer non-critical work (analytics initialization, prefetching) until after the first screen renders (
InteractionManager, orrequestIdleCallback-style scheduling). - Minimize synchronous storage reads at launch (MMKV is much faster than AsyncStorage), and keep the JS bundle lean by auditing dependencies.
Memory
Watch for leaked listeners and timers (clean up in effects), large caches held in JS, and images kept alive. Use profilers to compare heap snapshots before and after navigating screens repeatedly.
Best Practices
Test on Low-End Android Devices
Mid- and low-range Android phones are far slower than flagship iPhones. If it's smooth there, it's smooth everywhere. Include them in QA and performance testing.
Set Performance Budgets
Track startup time, frame rates on key screens, and bundle size in CI or release checks, with automated measurement where possible, so regressions are caught early.
Move Work Off the JS Thread
Heavy computation (parsing large data, image processing, crypto) belongs in native modules, worklets, or the backend. Paginate and pre-process data server-side.
Keep Dependencies Lean
Every library adds bundle size and startup cost. Prefer lightweight, maintained libraries, and remove unused ones.
Common Mistakes
Inline renderItem and Unstable Props
New functions on every render defeat memoization of rows. Define stable callbacks, and memoize row components (or rely on the React Compiler).
console.log in Production
Logging large objects in hot paths is surprisingly expensive. Strip console statements in release builds (a Babel plugin), and use a proper logger with levels.
Animating Layout From JavaScript
Updating width or top via state on every frame forces re-renders and layout recalculation on the JS thread. Use Reanimated with transforms instead.
FAQ
Why is my React Native app slow in development?
Development builds include extra validation, debugging hooks, and unoptimized JavaScript, so they're significantly slower than release builds. Always evaluate performance in a release build on a real device.
FlatList or FlashList?
FlashList for most long or complex lists: its view recycling gives smoother scrolling and lower memory use, especially on Android. FlatList is fine for short lists, or when you need a feature FlashList lacks, but tune its windowing props.
How do I make animations smooth?
Run them on the UI thread with Reanimated (shared values, worklets, layout animations) and handle gestures with react-native-gesture-handler. Animate transforms and opacity rather than layout properties, and avoid triggering React re-renders on every frame.
How can I reduce app startup time?
Use Hermes, lazy-load screens and heavy dependencies, defer non-essential initialization until after the first render, use fast synchronous storage like MMKV sparingly, and keep bundle size small. Measure cold start times on real devices to verify gains.
Related Topics
- React Native — The framework overview
- React Native Architecture — Threads, JSI, and Fabric
- React Performance — Rendering principles shared with React
- React Compiler — Automatic memoization
- Mobile State Management — Stores that minimize re-renders
- Profiling — Finding hot spots