Skip to main content

React Native Performance Debugger

Goals

  • Identify slow renders, JS thread stalls, and heavy network or disk work.
  • Measure and reproduce issues reliably.
  • Apply targeted fixes and verify improvements.

Core Tooling

  • Flipper with React DevTools and Performance plugins.
  • Hermes engine with Chrome DevTools integration (RN ≥ 0.71 defaults to Hermes).
  • Android Studio Profiler and Xcode Instruments for native CPU/Memory/Network.
  • React Profiler to audit component render costs.

Setup Checklist

  1. Enable Hermes (if not already):
    • iOS: set hermes_enabled: true in ios/Podfile and pod install.
    • Android: ensure enableHermes: true in android/app/build.gradle.
  2. Install Flipper and plugins:
    • Download Flipper, enable React DevTools, Network, and Performance plugins.
  3. Install React DevTools (optional for standalone):
    • npm i -D react-devtools and run npx react-devtools.

Profiling Workflow

1) Baseline metrics

  • Record app start time, first meaningful interaction, and frame rate.
  • Use Performance Monitor (Dev Menu → Perf Monitor) to watch FPS/UI/JS.

2) JS thread stalls

  • In Flipper, open Performance → JS Thread Activity.
  • Look for long tasks (>50ms). Typical culprits: heavy JSON parsing, large loops, synchronous storage.

3) Render overwork

  • Use React Profiler to record interactions. Identify components with many renders or long render times.
  • Fixes:
    • Memoize expensive calculations with useMemo.
    • Stabilize callback identities with useCallback.
    • Avoid inline anonymous functions in hot lists and renders.
    • Use React.memo for pure components.
Stabilize heavy props
const ExpensiveList = React.memo(({ items }: { items: Item[] }) => {
// render items
});

export function Screen({ data }: { data: Item[] }) {
const processed = useMemo(() => heavyProcess(data), [data]);
const onPress = useCallback((id: string) => handlePress(id), []);
return <ExpensiveList items={processed} />;
}

4) Lists

  • Prefer FlatList/SectionList with keyExtractor, getItemLayout, and initialNumToRender tuned.
  • Use removeClippedSubviews={true} for large lists.
  • Avoid scrollToIndex on huge lists without getItemLayout.

5) Network and storage

  • In Flipper Network tab, check slow endpoints and large payloads.
  • Cache results and paginate; avoid blocking the JS thread with synchronous storage.

6) Native hotspots

  • Android Studio Profiler: CPU/Memory → record during slow action.
  • Xcode Instruments: Time Profiler and Allocations.

Detecting Re-renders

Use why-did-you-render during development:

if (__DEV__) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const wdyr = require('@welldone-software/why-did-you-render');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const ReactInstance = require('react');
wdyr(ReactInstance, { trackAllPureComponents: true });
}

Performance Budget

  • JS long task budget: < 50ms per frame.
  • First screen interactive: < 2s on mid-range devices.
  • List scroll FPS: 55–60fps with no stutters.

Common Pitfalls

  • Unbounded state in global stores causing cascade re-renders.
  • Deriving large computed structures on each render.
  • Heavy images without caching/resizing; use FastImage or preprocessed assets.
  • Excessive layout passes; prefer flexbox simplicity and avoid nested views.

Verification

  • Re-run Profiler traces after fixes; compare render time deltas.
  • Use A/B flags in code to validate improvement quickly.
  • Automate perf smoke tests on CI with lightweight launch-time metrics.

References

  • React Native Performance docs
  • Flipper plugins and setup
  • Android and iOS native profiling guides