skip to content

Why do performance numbers from a React Native debug build mislead you, and which build should you profile instead?

level: middleimportance: must knowfreq 62%

answer

  1. dev mode does extra work
  2. __DEV__ checks and warnings
  3. JS from Metro, no bytecode
  4. debugOptimized fixes native only
  5. release-like build on a real device

basics

~20 s

A debug build runs development-mode JavaScript served by Metro, with dev-only checks and warnings and no Hermes bytecode precompilation, so the JS thread does far more work than in production. Profile a release or profileable release-like build on a real device.

solid answer

~40 s

In a debug build `__DEV__` is true, so React and React Native run extra validation and warnings, and the JavaScript is plain source loaded from Metro that Hermes compiles on the device. A release build bundles the JavaScript with `__DEV__` false and precompiles it to Hermes bytecode, and its native code is compiled with optimisations. The React Native docs are blunt: JS-thread performance suffers greatly in dev mode, so always test performance in release builds. Debug profiles are still useful for the *shape* of a problem — which function or component dominates — but not for its size. Since 0.82, Android's `debugOptimized` build type compiles the native C++ with optimisations while keeping JavaScript debugging, which speeds up animations and rendering in development; the JavaScript itself still runs in dev mode.

code

bash · 9 lines
bash
# Android debug build with optimised native code (JavaScript still in dev mode)
npx react-native run-android --mode debugOptimized
npx expo run:android --variant debugOptimized

# Release builds for real measurements
npx react-native run-android --mode release
npx react-native run-ios --mode Release
npx expo run:android --variant release
npx expo run:ios --configuration Release

go deeper

for a junior

Remember the rule from the React Native docs: never judge performance in a debug build; test in a release build on a real device.

for a middle

Explain the mechanics: DEV checks, JavaScript served by Metro and compiled on device versus precompiled Hermes bytecode, and what debugOptimized changes on Android.

for a senior

Show how you still use debug profiles to find candidates, then confirm size and fix in a release-like build with native traces or performance.measure.

for a principal

Weigh how much release-build profiling a team can afford: a profileable build in CI, reference devices, and when a debug-only regression report is enough to act on.

## What a debug build actually runs A React Native **debug build** is set up for iteration, not speed: - **`__DEV__` is `true`.** React and React Native run development-only code: extra validation, warnings, LogBox reporting and the hooks that React Native DevTools and Fast Refresh rely on. - **The JavaScript comes from Metro.** It is served as plain source over the network, and **Hermes** compiles it on the device at run time instead of loading precompiled bytecode. - **The native side is built for debugging.** On Android the default `debug` build type compiles React Native's C++ without optimisations so a native debugger can step through it. The React Native performance guide says it plainly: *JavaScript thread performance suffers greatly when running in dev mode… Always make sure to test performance in release builds.* A screen that drops frames in debug may be perfectly smooth in release, and a 30 ms render in debug says little about the release figure. ## What does not change between builds It helps to be precise about what the build type does **not** alter, because weak answers blame the wrong thing: - **The architecture.** Debug and release both run the New Architecture — the only one since 0.82 — with Fabric and Turbo Modules; there is no bridge in either. - **The thread model.** JavaScript runs on the JavaScript thread and native views are mounted on the main thread in both. - **The engine.** Both use Hermes; what differs is whether it receives source or precompiled bytecode. So a debug-versus-release gap is a difference in **how much work** the same pipeline does, not a different pipeline. ## What a release build changes | | `debug` | `debugOptimized` (Android) | `release` | |---|---|---|---| | JavaScript source | served by Metro, dev mode | served by Metro, dev mode | bundled into the app, `__DEV__` false | | Hermes input | source compiled on device | source compiled on device | bytecode compiled at build time | | Native C++ | unoptimised, debuggable | compiled with optimisations | optimised | | React Native DevTools | available | available | disabled | | C++ native debugger | available | not available | not available | ## Android's `debugOptimized` build type React Native 0.82 introduced **`debugOptimized`** (backported to 0.81 and Expo SDK 54). The React Native Gradle plugin creates it by copying `debug` and building its native code with `-DCMAKE_BUILD_TYPE=Release`, falling back to `release` variants of dependencies. It is listed with `debug` in the plugin's default `debuggableVariants`, so no JavaScript bundle is packaged and the app still loads JavaScript from Metro. - **What it fixes:** native overhead. The 0.82 release post shows a sample animation at roughly 20 FPS in `debug` against roughly 60 FPS in `debugOptimized`. - **What it does not fix:** dev-mode JavaScript. `__DEV__` is still true, so JS-thread numbers stay inflated. - **What it costs:** you cannot attach the C++ debugger; switch back to `debug` for that. Run it with `npx react-native run-android --mode debugOptimized` or `npx expo run:android --variant debugOptimized`. There is no iOS equivalent build type. ## Building something worth measuring 1. **Use a release or profileable, release-like build.** Bare projects: `npx react-native run-android --mode release` and `npx react-native run-ios --mode Release`. Expo projects with native folders: `npx expo run:android --variant release` and `npx expo run:ios --configuration Release`. 2. **Use a physical device**, ideally one at the low end of your audience. Simulators and emulators run on desktop CPUs. 3. **Capture with tools that work there**: Instruments or the Android Studio system trace for threads, and `performance.mark` / `performance.measure` read through `PerformanceObserver`, which work in production builds since 0.83. ## Using debug profiles without being fooled Debug profiles remain the fastest way to find *candidates*. A React Native DevTools recording of the sports-scores screen can show that one scores formatter dominates JavaScript time. Treat that as a hypothesis: - **Discount dev-only frames.** Validation and warning code inflates React's own share; it is absent in release. - **Compare relative weight, not absolute milliseconds.** If your function is heavy relative to your other code, it is probably heavy in release too. - **Confirm in release.** Wrap the suspect in a `performance.measure`, or compare the JavaScript thread's slices in a native trace before and after the fix. ## Common mistakes - Filing a jank bug from a debug build without reproducing it in release. - Believing `debugOptimized` gives release-level JavaScript numbers. - Expecting React Native DevTools to attach to the store build. - Measuring on an emulator and reporting the result as device performance.

  • What exactly does React Native's Android debugOptimized build type change?
    The Gradle plugin creates it from `debug` but compiles native C++ with `-DCMAKE_BUILD_TYPE=Release` and falls back to release variants of dependencies. It is still a debuggable variant, so JavaScript loads from Metro in dev mode and React Native DevTools works; only the C++ debugger is lost. Native overhead drops, dev-mode JavaScript overhead stays.
  • If React Native DevTools only profiles debug builds, is its JavaScript profile worthless?
    No. It reliably shows the shape of a problem — which of your functions or components dominate the JavaScript thread — even though every number is inflated. Use it to form a hypothesis, discount React's dev-only frames, and then confirm the size of the problem in a release build with `performance.measure` or a native trace.
  • Why does a release build start faster with Hermes than a debug build?
    A release build compiles the JavaScript bundle to Hermes bytecode at build time, so the device loads bytecode instead of parsing and compiling source. A debug build receives plain source from Metro and compiles it on the device, on top of running dev-mode checks.

saying these in an interview costs you the question

  • Debug and release builds run the same JavaScript, so timings match
  • debugOptimized gives release-level JavaScript performance
  • React Native DevTools can attach to a store release build
  • Debug builds are slow only because DevTools is connected
  • An emulator profile is a fair stand-in for a mid-range phone