skip to content

In React Native, how do you confirm Hermes is running, and why can that check pass while a transit-timetable app still cold-starts slowly?

level: middleimportance: should knowfreq 38%

answer

  1. a global only the engine defines
  2. HermesInternal: null or an object
  3. present does not mean bytecode
  4. debug bundles come from Metro
  5. measure the release variant

basics

~20 s

Check the HermesInternal global, which only Hermes defines. It proves the engine, not the input: debug builds run development source from Metro, and non-standard bundle loading can skip the precompiled bytecode, so measure a release build that ships the .hbc.

solid answer

~50 s

React Native's docs give one runtime check: Hermes defines a global named `HermesInternal`, so `typeof HermesInternal === 'object' && HermesInternal !== null` is true only on Hermes (new projects also show the engine on the welcome screen). That proves which **engine** runs, not what it was **fed**. A debug build normally loads development-mode JavaScript source from Metro, which Hermes has to compile on the phone, so a cold start measured there is meaningless. The docs also warn that if a project loads its bundle in a non-standard way, `HermesInternal` can be present while the precompiled bytecode is not used. So for a slow cold start on a low-end phone, first confirm you are timing a **release** variant, then confirm the packaged bundle is the `.hbc` output rather than JavaScript text, and only then look at the app's own startup work.

code

tsx · 7 lines
tsx
import { Text } from 'react-native';

export function EngineBadge() {
  const usingHermes =
    typeof HermesInternal === 'object' && HermesInternal !== null;
  return <Text>{usingHermes ? 'Engine: Hermes' : 'Engine: not Hermes'}</Text>;
}

go deeper

for a junior

Know the HermesInternal global as the documented check that Hermes is the engine, and that new projects show the engine on the welcome screen.

for a middle

Explain why the check proves the engine but not the input: debug builds load source from Metro, and non-standard loading can skip the precompiled bytecode.

for a senior

Run the diagnosis in order: engine, build type, packaged bytecode, then the app's own startup work, measuring release against release on the slow device class.

for a principal

Make release-build startup measurement on low-end hardware part of the team's routine, so engine assumptions are verified by numbers rather than asserted in review.

## The check React Native documents The **Hermes** engine exposes a global object named **`HermesInternal`**. Other engines do not define it, so its presence is the documented way to know, from JavaScript, that Hermes is executing your code. The React Native docs write it as `!!global.HermesInternal`. In a TypeScript project, React Native's own global type declarations describe it as `const HermesInternal: null | {}` rather than a property of some `global` object, so the idiomatic typed form is a `typeof` guard: - `typeof HermesInternal === 'object' && HermesInternal !== null` is **true on Hermes**; - on another engine the identifier does not exist, and `typeof` returns `'undefined'` instead of throwing a `ReferenceError`. React Native itself uses the same object internally, for example to detect Hermes's built-in `Promise` support. A freshly created app also shows whether Hermes is enabled on its welcome screen. ## What the check does not prove `HermesInternal` answers "which engine?" It does not answer "did the engine receive precompiled bytecode?" That second question is where Hermes's startup benefit lives, because Hermes is fast to start only when the release build has already compiled the bundle with `hermesc`. There are two ordinary ways for the check to pass while the benefit is missing: 1. **You are measuring a debug build.** Debug builds normally fetch the bundle from the **Metro** dev server as development-mode JavaScript source. Hermes compiles that source on the device, and the development bundle carries extra checks and warnings. On an iOS simulator the Debug configuration skips packaging a bundle entirely, because Metro serves it. 2. **The bundle is loaded in a non-standard way.** The docs warn that with a custom bundle-loading setup `HermesInternal` can be available while you are not using the optimised bytecode, and tell you to confirm the `.hbc` file is what the app loads and to benchmark before and after. ## Applying it to a slow cold start Picture a **transit-timetable app**: riders open it at a bus stop on a cheap Android phone and it takes too long to show the next departures. A teammate says "Hermes must be off". A disciplined check goes in this order: 1. **Confirm the engine.** Log or display the `HermesInternal` check in a build you can inspect. If it is false, the build was configured away from Hermes (in React Native 0.87 that means a deliberate move to the community JavaScriptCore package). 2. **Confirm the build type.** Time the **release** variant (the docs suggest `npm run android -- --mode="release"`), never a debug build served by Metro. 3. **Confirm the input.** In a standard release build the Gradle plugin compiles `index.android.bundle` to bytecode with `hermesc`. The packaged file should be binary bytecode, not readable JavaScript. If the project replaced the standard bundling with its own, verify its output the same way. 4. **Only then profile the app itself.** If Hermes is running precompiled bytecode and start-up is still slow, the cause is the work the app does before the first screen, which is a startup-tuning problem rather than an engine problem. | Observation | What it tells you | |---|---| | `HermesInternal` absent | Not running on Hermes at all | | `HermesInternal` present, debug build | Hermes, but compiling development source on the device | | `HermesInternal` present, custom loading, bundle is JS text | Hermes, but no precompiled bytecode | | `HermesInternal` present, release build, bundle is bytecode | Engine is doing its job; look at the app's own startup work | ## Traps worth naming - **`__DEV__` is not an engine check.** It tells you whether the bundle is a development or production bundle, on any engine. - **A file name is not proof.** iOS names the packaged bundle `main.jsbundle` whether it holds source or bytecode, and Android keeps `index.android.bundle` after moving the bytecode over it. - **Minification is irrelevant here.** Release builds with Hermes deliberately skip minifying the bundle; an unminified bundle in a Hermes build is expected. - **Comparing across build types misleads.** A debug-versus-release comparison mixes the engine's input with development-mode overhead, so compare release against release. ## Summary `HermesInternal` is the quick, documented way to confirm the engine. Hermes's startup benefit, though, depends on the release build shipping `hermesc` output, so the check has to be paired with the right build type and a look at what was actually packaged.

  • Why is the typeof guard safer than referencing HermesInternal directly?
    On an engine other than Hermes the identifier is simply undeclared at runtime. Reading an undeclared identifier throws a `ReferenceError`, while `typeof` on it returns `'undefined'`. The guard therefore works on every engine, and the `!== null` part matches React Native's declared type of `null | {}`.
  • The check passes in release and the bundle is bytecode, but cold start is still slow on low-end phones. What now?
    Then the engine is not the bottleneck. Hermes has removed parse and compile work, and what remains is the app's own startup path: code executed before the first screen, eager imports, synchronous storage reads, heavy initial renders. That is startup tuning, measured with a profiler on a release build.

saying these in an interview costs you the question

  • __DEV__ being false proves the app runs on Hermes
  • HermesInternal present means the app is using precompiled bytecode
  • Timing cold start in a debug build is a fair Hermes benchmark
  • A file named main.jsbundle must contain JavaScript source
  • An unminified bundle in a Hermes release build signals a misconfiguration