---
name: react-native-performance
description: Diagnose and fix React Native performance — slow app startup, dropped frames, janky FlatList/FlashList scrolling, laggy animations and gestures, JS thread blocking, large bundles, memory leaks, and image loading. Use this whenever the user says a React Native or Expo app is slow, stutters, has low FPS, takes long to open, or asks about Reanimated, Gesture Handler, FlashList, Hermes, the New Architecture's performance, profiling with Flipper/Perfetto/Xcode Instruments, or app size. Also apply it when reviewing RN code that renders long lists, animates with Animated API on the JS thread, or does heavy work in render.
metadata:
  technology: React Native
  type: performance
---

# React Native Performance

Two threads matter: the **JS thread** (your React code) and the **UI thread** (native rendering). Anything that blocks the JS thread for > 16 ms drops frames. Most RN performance work is moving work off the JS thread or doing less of it.

## 1. Measure first — the right tool for each symptom

| Symptom | Tool | Capture |
|---|---|---|
| Slow cold start | `react-native-performance` marks, Xcode Instruments (App Launch), Android `adb shell am start -W` | Time to first screen, time to interactive |
| Dropped frames while scrolling/animating | Perf Monitor overlay (dev menu), Perfetto (Android), Instruments Core Animation (iOS) | JS FPS vs UI FPS — tells you which thread is the bottleneck |
| Slow interactions | React DevTools Profiler, Hermes sampling profiler (`--profile`), Flipper/Reactotron | Long JS commits, render counts |
| Big binary / slow OTA | `npx react-native-bundle-visualizer`, EAS build size report | Largest JS modules, native libs, asset weights |
| Memory growth | Xcode Memory Graph, Android Studio Profiler | Retained images, listeners |

Always profile a **release** build on a **low-end real device** (an Android from ~3 years ago). Dev mode is 5–10× slower and hides everything.

## 2. Startup time

- **Hermes on, New Architecture on.** Check both; they are the biggest free wins.
- Defer everything not needed for first paint: analytics init, remote config, non-critical native modules — start them after `InteractionManager.runAfterInteractions` or on first idle.
- Lazy-require heavy screens/libraries (`React.lazy` with Expo Router's automatic splitting, or inline `require()` in navigation).
- Reduce root re-renders: one provider tree, memoised context values, no fetching in the root layout that blocks render.
- Splash: use `expo-splash-screen` to hold the native splash until fonts/session are ready, then hide once — no flash of unstyled UI.
- Inline requires (`inlineRequires: true` in Metro transformer) — on by default in modern templates; verify.

## 3. Lists — the #1 jank source

- Use **FlashList** (`@shopify/flash-list`) for any list > 50 items. Provide `estimatedItemSize`. Keep `keyExtractor` stable.
- If staying on `FlatList`: `getItemLayout` for fixed heights, `windowSize` 5–10, `maxToRenderPerBatch`, `removeClippedSubviews` on Android, `initialNumToRender` sized to one screen.
- Row components: `memo`, no inline closures creating new props, no `new Date()`/formatting in render — pre-format in the data layer.
- Images inside rows: fixed dimensions, `expo-image` with `recyclingKey`, thumbnails not originals.
- Never nest a vertical list inside a `ScrollView`. Use list header/footer components or `SectionList`.
- Avoid `key` = index on reorderable data.

## 4. Animations and gestures — UI thread only

- **Reanimated 3+** for everything animated. `useAnimatedStyle` + `withSpring`/`withTiming` run on the UI thread; the JS thread can be frozen and the animation still plays.
- **Gesture Handler** with Reanimated worklets for drag, swipe, pinch. Never `PanResponder`.
- Layout animations via Reanimated `Layout`/entering/exiting; avoid `LayoutAnimation` from core.
- Don't `setState` per frame. Use shared values; only sync to React state on gesture end (`runOnJS` once).
- `Animated` (core) only with `useNativeDriver: true`, which supports opacity/transform only — that limitation is why Reanimated is the default.
- Skia (`@shopify/react-native-skia`) for complex drawing/charts that would otherwise be hundreds of `View`s.

## 5. Rendering and JS thread hygiene

- Apply the React re-render decision tree (see `react-performance`): move state down, split contexts, then memo. React Compiler works in RN too.
- Heavy computation (search, sorting large arrays, parsing) → move to the data layer once, cache with TanStack Query/`useMemo`, or offload to a worklet/JSI module.
- Avoid `console.log` in production paths — strip with `babel-plugin-transform-remove-console`.
- Keep the component tree shallow: each `View` is a native view; flatten wrappers, prefer `StyleSheet` composition over nested containers.
- Measure and avoid `onLayout` cascades that trigger re-renders every frame.

## 6. Images

- **`expo-image`** everywhere: disk + memory caching, blurhash placeholders, `contentFit`, priority, prefetch.
- Serve correctly sized images from a CDN with resizing params — never download 4000 px for a 100 px avatar.
- `cachePolicy="memory-disk"` for repeated content; `recyclingKey` inside lists.
- Local assets: compress (WebP), and preload critical ones with `Asset.loadAsync` behind the splash.

## 7. Network and data

- Persist the TanStack Query cache (MMKV) so screens render instantly from cache, then refetch.
- `staleTime` > 0 for anything that doesn't need to be live; avoid refetch on every focus for expensive endpoints.
- Batch and paginate; avoid N+1 request patterns in lists.
- Compress payloads (gzip/brotli), request only fields needed.
- Use `react-native-mmkv` not AsyncStorage for anything read during render.

## 8. Bundle and binary size

- Analyse with bundle visualizer; remove moment, full lodash, unused icon sets, duplicated polyfills.
- Enable Proguard/R8 (Android) and bitcode-free, stripped builds (iOS); use App Bundles (`.aab`).
- Hermes bytecode precompilation is on in release — confirm.
- Strip unused locales/fonts; move large media to remote download on demand.
- Check `expo-doctor` and dependency count; every native lib adds startup cost.

## 9. Memory

- Remove listeners in effect cleanup (`AppState`, `Dimensions`, `Keyboard`, `NetInfo`, event emitters).
- Clear timers and cancel in-flight requests on unmount (AbortController).
- Unbounded caches (in-memory maps of images or records) need eviction.
- Large base64 strings in state are a leak magnet — store files with `expo-file-system` and keep URIs.

## 10. Report format

```
## Device & build
<model, OS>, release build, New Arch: on/off, Hermes: on/off

## Baseline
JS FPS / UI FPS during <scenario>; cold start ms; bundle MB

## Findings (impact order)
1. <what> — <evidence> — <thread affected>

## Fixes
Minimal diff per finding + expected effect

## Verify
Same scenario, same device, after/before numbers
```

## Anti-patterns to reject on sight

- `ScrollView` + `.map()` over hundreds of items.
- `Animated.Value` driven by `setState` per frame, or any animation on the JS thread.
- `PanResponder` for new gesture code.
- Measuring in debug mode on a simulator.
- Images without explicit dimensions or with originals rendered as thumbnails.
- AsyncStorage reads on the render path.
- Disabling Hermes or the New Architecture to "fix" a crash instead of fixing the dependency.
