Why do performance numbers from a React Native debug build mislead you, and which build should you profile instead?
answer
- dev mode does extra work
- __DEV__ checks and warnings
- JS from Metro, no bytecode
- debugOptimized fixes native only
- release-like build on a real device
basics
~20 sA 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 sIn 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# 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 Releasego deeper
Remember the rule from the React Native docs: never judge performance in a debug build; test in a release build on a real device.
Explain the mechanics: DEV checks, JavaScript served by Metro and compiled on device versus precompiled Hermes bytecode, and what debugOptimized changes on Android.
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.
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