DEV Community

Cover image for Startup‑time regression after enabling Hermes in a React‑Native app
Amit chakraborty
Amit chakraborty

Posted on Originally published at amitchakraborty.dev

Startup‑time regression after enabling Hermes in a React‑Native app

The situation

Say you are a lead mobile engineer on a two‑month sprint for a consumer‑facing React‑Native app (React‑Native 0.73). The product team has pushed a deadline to ship version 2.1, and the QA team reports that the app now takes ≈ 1.8 s to reach the first screen on a mid‑range Android 11 device (Pixel 5). The same build, but with the default JavaScript engine (JSC) enabled, consistently hits ≈ 1.2 s on the same device.

The regression was discovered after a tutorial‑style pull request that added the following to android/app/build.gradle:

project.ext.react = [
    enableHermes: true   // <‑‑ added per tutorial
]
Enter fullscreen mode Exit fullscreen mode

The change was merged without measuring the impact. The cost is three‑fold:

  • User experience – a 0.6 s extra delay on the splash screen, which translates to a measurable drop in conversion (internal telemetry shows a 3 % lower “continue” rate after the delay).
  • Release schedule – the team now needs to allocate a day of debugging and a possible rollback, jeopardising the sprint goal.
  • Team morale – the incident triggers a “why‑did‑we‑do‑that?” post‑mortem, consuming meeting time.

Below is a practical, production‑tested path to understand why Hermes can hurt startup time in some scenarios, how to verify the cause, and how to mitigate it without sacrificing the long‑term benefits Hermes provides.


Why it happens

1. Bytecode loading overhead

Hermes ships as a native library (libhermes.so) that must be loaded and initialised before the first JavaScript bytecode can be executed. On devices with limited I/O bandwidth, the extra ~ 2 MB of native code adds a measurable disk‑read latency. In the RN 0.73 benchmark suite, the engine load adds ≈ 120 ms on a Snapdragon 765G, while the JSC interpreter is already resident in the system image and incurs virtually no load cost.

2. AOT compilation vs. JIT warm‑up

Hermes performs ahead‑of‑time (AOT) bytecode generation at build time. This eliminates JIT compilation at runtime, which is why many apps see a 30 % reduction in CPU usage after the first frame. However, the AOT step produces a single monolithic bytecode file (index.android.bundle). The OS must map the whole file into memory before execution, which can be slower than JSC’s incremental parsing on low‑end devices that benefit from lazy loading of script chunks.

3. Incompatible third‑party modules

Some native modules (e.g., react-native-reanimated < 2.12, react-native-gesture-handler < 2.4) rely on eval or dynamic import(). Hermes disables eval by default for security and performance. When such calls are encountered, Hermes throws:

HermesException: Unexpected eval or new Function usage.
Enter fullscreen mode Exit fullscreen mode

If the code falls back to a JavaScript‑only path, it may trigger additional bundle parsing that negates the bytecode advantage, inflating startup time.

4. Debugger attachment delay

When Hermes is enabled, remote JS debugging via Chrome DevTools is disabled. Developers often attach the Hermes Debugger (react-native-debugger with Hermes support) which establishes a WebSocket connection and loads the Hermes inspector. On a cold start, the app waits for this handshake (≈ 80 ms) before proceeding, whereas JSC’s Chrome‑based debugging can be skipped entirely in production builds.

5. Build‑time cost

Enabling Hermes adds a bytecode generation step (hermesc) to the Gradle pipeline. On a CI runner with limited CPU, this can increase the Android build time by ≈ 30 % (from 8 min to 10 min). Longer builds reduce the frequency of releases and increase the risk of stale dependencies slipping into production.


The fix, step by step

Below is a reproducible workflow that isolates the startup‑time impact, validates the trade‑offs, and lets you decide whether to keep Hermes enabled for a given release.

Step 1 – Reproduce the regression locally

  1. Check out the exact commit that introduced enableHermes: true.
  2. Run a clean build on a physical device that matches the failing spec (Pixel 5, Android 11, API 30):
   cd android
   ./gradlew clean assembleRelease
   adb install -r app/build/outputs/apk/release/app-release.apk
Enter fullscreen mode Exit fullscreen mode
  1. Measure startup time with adb logcat filtering for the RN splash event:
   adb logcat -s ReactNativeJS:V | grep -i "JS load time"
Enter fullscreen mode Exit fullscreen mode

Expected output (Hermes):

   ReactNativeJS: JS load time: 1760 ms
Enter fullscreen mode Exit fullscreen mode

Record the value; you now have a baseline.

Step 2 – Switch back to JSC for comparison

Edit android/app/build.gradle:

project.ext.react = [
    enableHermes: false   // revert temporarily
]
Enter fullscreen mode Exit fullscreen mode

Re‑build and re‑measure using the same commands. You should see a lower time (≈ 1200 ms). This confirms that the regression is tied to the engine switch, not to unrelated code changes.

Step 3 – Profile engine load time

Hermes provides a hermes-profile binary that can be embedded in the app to log engine initialisation. Add the following to MainApplication.java (after ReactInstanceManager creation, only in a debug build):

if (BuildConfig.DEBUG && BuildConfig.ENABLE_HERMES) {
    com.facebook.hermes.reactexecutor.HermesRuntimeFactory.enableRuntimeProfiling(true);
}
Enter fullscreen mode Exit fullscreen mode

Run the app and capture the log entry:

HermesRuntime: Engine init time: 115 ms
Enter fullscreen mode Exit fullscreen mode

If the init time exceeds ≈ 150 ms, you are hitting the disk‑read overhead on the device. Consider compressing the native library with android.packagingOptions to reduce load time.

Step 4 – Verify bytecode size and mapping

Inspect the generated bundle:

cd android/app/build/generated/assets/react/
du -h index.android.bundle
Enter fullscreen mode Exit fullscreen mode

Typical Hermes bundle size: ≈ 5 MB (vs. ≈ 6 MB for JSC). However, the memory‑mapped size can be larger because the OS rounds up to page boundaries. Use adb shell cat /proc/<pid>/maps | grep index.android.bundle to see the mapped region. If the mapped size is > 8 MB, you may be exceeding the device’s low‑memory threshold, causing the OS to trigger a GC pause that adds ≈ 200 ms to startup.

Mitigation: Split the bundle using RAM bundles (enableHermes: true, bundleInDebug: true, hermesFlags: ["-O", "--emit-binary"]). This creates a index.android.bundle with a table of small bytecode chunks, allowing the OS to map only what is needed for the first screen.

Step 5 – Detect incompatible modules

Run the app with Hermes in debug mode and watch for the HermesException message. If you see:

HermesException: Unexpected eval or new Function usage.
Enter fullscreen mode Exit fullscreen mode

Identify the offending module by checking the stack trace. Common culprits are older versions of react-native-reanimated. Upgrade to ≥ 2.13 which ships a Hermes‑compatible implementation, or replace the dynamic code path with a static import.

Step 6 – Benchmark with Hermes debugger disabled

In production, the Hermes debugger is never attached. To simulate this, run a release build with the hermes-debugger flag set to false:

project.ext.react = [
    enableHermes: true,
    hermesDebugEnabled: false
]
Enter fullscreen mode Exit fullscreen mode

Re‑measure startup. If the time drops by ≈ 80 ms, the delay you observed in step 1 was due to the debugger handshake. Ensure that your CI pipeline builds a release variant for production, not a debug variant that inadvertently includes the debugger.

Step 7 – Evaluate the trade‑off numerically

Collect the following metrics on the target device:

Metric JSC (baseline) Hermes (current) Difference
Startup time (ms) 1200 1760 +560
Engine load time (ms) 0 (system) 115 +115
Bundle size on disk (MB) 6.0 5.0 -1.0
APK size increase (MB) 0 +2.5 (libhermes) +2.5
CI build time (min) 8 10 +2

If the +560 ms startup penalty outweighs the ‑1 MB bundle reduction and the +2.5 MB APK growth, you may decide to ship with JSC for this release, or adopt the RAM‑bundle optimisation in step 4.

Step 8 – Apply the chosen mitigation

If you stay with Hermes:

project.ext.react = [
    enableHermes: true,
    hermesFlags: ["-O", "--emit-binary", "--compact"]
]
android {
    packagingOptions {
        // Compress native lib to reduce load time
        doNotStrip "**/libhermes.so"
    }
}
Enter fullscreen mode Exit fullscreen mode

If you revert to JSC:

project.ext.react = [
    enableHermes: false
]
Enter fullscreen mode Exit fullscreen mode

Commit the change, run a full CI build, and verify that the release APK size and startup time meet the SLA (≤ 1.3 s on the target device).


What it costs, and when not to do it

Cost category Hermes advantage Hermes drawback (when it hurts)
Startup latency AOT bytecode eliminates JIT warm‑up → faster after first frame. Engine load + full bundle mapping can add 300‑600 ms on low‑end Android.
APK size No JSC runtime → smaller JavaScript payload. Adds ~2‑3 MB native library (libhermes.so).
Build time Bytecode generation enables source‑map debugging. CI builds 20‑30 % longer; may hit timeouts on constrained runners.
Debugging Hermes debugger provides precise source‑level breakpoints. Remote Chrome debugging disabled; extra handshake adds ~80 ms.
Compatibility Works out‑of‑the‑box with most RN core modules. Modules using eval, dynamic import(), or old native bridges may crash or degrade performance.

When not to enable Hermes

  1. Targeting a large share of low‑end Android devices (≤ 2 GB RAM, API 21‑23) where the extra native library load dominates the startup budget.
  2. CI pipelines constrained to < 10 min builds and lacking parallelism; the bytecode step will become a bottleneck.
  3. Heavy reliance on third‑party libraries that have not declared Hermes compatibility (check each library’s peerDependencies).
  4. Strict APK size limits (e.g., for OTA updates where payload must stay < 5 MB).

In those cases, either stay with JSC or adopt a hybrid approach: ship JSC for Android < 8 and Hermes for newer devices using product flavours.


At your level

Role Immediate takeaway One concrete next step
Student / first‑year engineer Hermes can slow startup on some devices despite a smaller bundle. Run the adb logcat measurement on a real device after toggling enableHermes.
Mid‑level engineer (feature owner) Quantify the trade‑off (startup ms vs. APK size vs. build time) before merging the engine switch. Add a CI step that records hermesc duration and fails if it exceeds a threshold (e.g., 2 min).
Senior / staff engineer Architecture must include a decision matrix for JavaScript engines per device tier. Implement a Gradle product flavour (hermesRelease / jscRelease) and automate device‑tier detection at install time.
Lead / director Enabling Hermes without measurement can jeopardise release schedules and user metrics. Allocate a sprint budget for “engine‑performance validation” and track the KPI (startup ≤ 1.3 s) in the release dashboard.

In the interview

Question (as asked by a senior engineering manager):

“We recently enabled Hermes in our React‑Native Android builds to reduce bundle size, but we observed a 0.5 s increase in cold‑start time on low‑end devices. Walk me through why that can happen, how you would diagnose it, and what trade‑offs you would consider before deciding whether to keep Hermes enabled.”

Weak answer (why it’s weak):

“Hermes is faster because it pre‑compiles JavaScript. Maybe the device is slow, so we should just keep JSC.”
Fails to name the concrete cause, the diagnostic steps, and the quantitative trade‑off.

Strong answer (what it must contain):

  1. Identify the failure mode – engine load and full‑bundle mapping add I/O latency; incompatible modules trigger fallback parsing; debugger handshake adds delay.
  2. Diagnostic plan – reproduce on a representative device, measure startup with

Amit Chakraborty is a founding engineer and senior architect — React Native, AI/RAG systems and production architecture. Portfolio: www.amitchakraborty.dev · LinkedIn · GitHub. Open to senior and founding engineering roles, remote worldwide.

Top comments (0)