React Native Performance & Build
Degree of freedom: MIXED. Which category and fix [HIGH freedom]; measure
before/after, 16KB alignment, and upgrade-helper diffs
[LOW freedom — run exactly].
The performance, bundling, and upgrade layer for React Native / Expo.
mobile-rn-screenhandles screen layout and native feel; this skill handles speed, size, and version moves.Distilled from Callstack's React Native best-practices skill (MIT) and the Ultimate Guide to RN Optimization.
How to reason
- Measure — RN perf monitor / profiler numbers first
- Category — FPS, bundle, TTI, native, memory, or animation
- Fix — the profiled hotspot only
- Re-measure — before/after on a real low-end device
Worked example
Measure: feed scroll drops to 38fps; React profiler shows every row re-rendering. Category: FPS & re-renders — not "add memo everywhere". Fix:
FlashList+ stablekeyExtractor; animation stays on Reanimated shared values. Re-measure: 56fps on a low-end Android; no new storm on the changed screen.
Self-critique before reporting
- Not vibes — a number existed before the patch
- Hotspot only — no unprofiled memo/lazy spray
- Both platforms — Android measured; iOS-affecting native changes verified on macOS CI
- Right owner — layout / safe-area / touch feel →
mobile-rn-screen; on-device walk →mobile-emulator-test; hybrid axes →enhance-capacitor-ui
Diagnose first, then fix [HIGH freedom]
Never optimize blind. Identify the symptom, then apply the matching category. Measure before/after.
| Symptom | Category | Impact |
|---|---|---|
| Janky scroll/animation, dropped frames | FPS & re-renders | CRITICAL |
| Large download / install size | Bundle size | CRITICAL |
| Slow cold start (high TTI) | TTI / startup | HIGH |
| Slow native operations | Native performance | HIGH |
| Memory grows over time / crashes | Memory management | MED-HIGH |
| Choppy gestures/transitions | Animations | MEDIUM |
1. FPS & re-renders (CRITICAL)
- Profile with React DevTools + the RN perf monitor before changing code. Find the component re-rendering, don't guess.
- Memoize hot paths:
React.memo,useMemo,useCallback— but only where a profile shows wasted renders (over-memoization adds its own cost). - Stable references: don't create new objects/arrays/functions inline in props of list rows or memoized children.
- Lists: use
FlashList(Shopify) overFlatListfor long/heterogeneous lists. Provide stablekeyExtractor, avoid anonymousrenderItemclosures, and give size hints. A FlatList re-render storm is the #1 RN list perf bug. - Move continuous values (scroll, gesture, animation) off React state — see Animations.
2. Bundle & app size (CRITICAL)
- Enable Hermes (default on modern RN) — smaller, faster startup.
- Audit the bundle: tree-shake, avoid pulling whole libraries for one function, lazy-load heavy screens.
- Ship per-architecture / split APKs; drop unused locales and assets.
- Android 16KB page-size alignment — required for Google Play; verify all third-party native libs are aligned or builds get rejected.
3. TTI / startup (HIGH)
- Defer non-critical work past first paint (analytics, feature flags, heavy init).
- Lazy-init native modules; don't block the JS thread on startup.
- Measure TTI on a real low-end device, not a flagship or simulator.
4. Native performance (HIGH)
- Prefer native (Turbo Modules / JSI) over JS polyfills for hot, frequent operations.
- Minimize bridge traffic: batch calls, avoid chatty per-frame round-trips.
- Profile with Xcode Instruments / Android Studio profiler for native hotspots.
5. Memory (MED-HIGH)
- Clean up listeners, timers, and subscriptions in effect cleanup. Leaked listeners are the common JS leak.
- Watch image caches and large in-memory data; page or virtualize.
- Profile native leaks with platform tools when JS looks clean.
6. Animations (MEDIUM)
- Use
react-native-reanimatedon the UI thread (useSharedValue/useAnimatedStyle/ worklets). Never drive continuous animation throughsetState— it re-renders the tree every frame and collapses FPS. - Use the native driver for
Animatedwhen staying on the core API.
React Native / Expo upgrades [LOW freedom — run exactly]
- Use the official RN Upgrade Helper diff for the exact version jump; apply native-side changes (
android/,ios/, Podfile, Gradle) deliberately — these are where upgrades break. - Bump Expo SDK via
expo install --fix; align allexpo-*and third-party native deps to the SDK's supported ranges. - After upgrade: clean caches (
watchman watch-del, Metro cache, Pods, Gradle), rebuild, and smoke-test on both platforms viamobile-emulator-test. - For Linux/Windows devs: iOS-affecting upgrade changes verify on macOS CI, not locally.
Definition of done (RN perf) [LOW freedom — do not skip]
- Measured before/after on a real device (numbers, not vibes).
- The fix targets the profiled hotspot, not a guess.
- No new re-render storms introduced (re-profile the changed screen).
- Android 16KB alignment holds if native libs changed.
- Both platforms still build and run.
Composes with
mobile-rn-screen— layout, safe-area, touch-target, native feel.mobile-emulator-start/mobile-emulator-test— boot + on-device verification.workflow-spec-tdd— spec + test the perf fix so it doesn't regress.enhance-capacitor-ui— if the app is hybrid rather than bare RN, the axis architecture differs.