Hermes bytecode vs. JSC plain-text JavaScript: different engines, different bundle formats, and different failure modes. This guide covers configuration, benchmarks, and every Hermes OTA pitfall we've encountered in production.
Both engines execute JavaScript, but the way they consume your bundle is fundamentally different. That difference dictates how OTA works, how quickly it applies, and how it can fail.
.jsbundle file. No compilation step. The engine parses and compiles JS on every cold start.hermesc compiler transforms JavaScript into .hbc bytecode at build time. The engine loads the bytecode directly, with no parsing required.Understanding the compilation pipeline is key to debugging any Hermes OTA failure. Here's the full chain, from source code to execution.
Metro resolves imports, tree-shakes dead code, and concatenates everything into a single index.bundle.js file. This is plain JavaScript, identical to what JSC would receive. Metro also generates a .map source map that maps the bundle back to your original source files.
hermesc compiles to bytecodeThe Hermes compiler (hermesc) takes the plain JavaScript bundle and compiles it into Hermes bytecode (.hbc). This step performs lexing, parsing, and compilation ahead of time. The output is a binary format that the Hermes VM can execute directly through memory mapping. hermesc also generates a second source map that maps bytecode offsets to JavaScript line numbers.
.hbc fileAt runtime, Hermes memory-maps the .hbc file directly into the process address space. There is no parsing and no JIT compilation. The VM reads bytecode instructions and executes them immediately. This is why Hermes delivers significantly faster cold starts. However, it also means that the bytecode version in the file must exactly match the Hermes VM version compiled into the app binary.
When an OTA update arrives, the new bundle overwrites the old one. However, Hermes stores compiled bytecode in a separate cache directory (for example, caches/hermes/). If this cache is not cleared, the VM may continue executing the old cached bytecode even though the new bundle exists on disk. This is one of the most common causes of Hermes OTA update failures.
Bytecode version mismatch error.. Always compile OTA bundles using the same hermesc version that was shipped with the app binary.Measured on a 45k-line React Native e-commerce app running on an iPhone 13 and a Pixel 7. Three OTA configurations were tested end-to-end.
Test app: 45K LoC, 180 screens, 42 native modules. Hermes bytecode compiled with hermesc -O (optimized). Network: throttled 4G (12 Mbps) and 3G (1.5 Mbps).
Metro generates JavaScript bundles, but it does not generate Hermes bytecode. In modern React Native (0.80+), the OTA workflow is: Metro → JavaScript Bundle → Hermes Compiler → Bytecode → Source Map Composition → OTA Upload. Understanding this pipeline helps prevent version mismatches, broken source maps, and oversized OTA updates.
hermesc binary for your React Native version, composes source maps, and uploads everything with a single command: appsonair-codepush release-react. No manual build scripts. No version mismatches.Every Hermes OTA bug we've seen in production, with the exact error message and the fix.
Error: Bytecode version mismatch.hbc file was compiled with a different Hermes version than the one included in the app binary. This commonly occurs when CI uses a different React Native version than the version used to build and release the app.Always use the hermesc binary from the same node_modules/react-native version that was used to build the app binary.
Symptom: crash reports show bytecode offsets, not file:lineUse compose-source-maps.js to combine the Metro and Hermes source maps. Upload the composed source map to both your crash-reporting service and OTA provider.
Symptom: OTA works but cold start regresses 2–4x.jsbundle to a Hermes-enabled app, Hermes will compile it on the device during the first launch. It works, but startup time can increase from approximately 340 ms to 820 ms. Users will notice the difference.Always compile your bundle to .hbc before uploading it. Alternatively, use AppsOnAir CodePush, which automatically detects Hermes and performs the compilation for you.
Error: crash on launch after OTAserializer.getModulesRunBeforeMainModule or RAM bundle flags enabled, the output is incompatible with hermesc.Disable RAM bundles when using Hermes. Hermes bytecode already gives you the lazy-loading benefit that RAM bundles were designed for.
Same engine, different platform integration. Here's what to watch for when deploying OTA bundles.
hermes.xcframework). The engine is linked into the app binary at build time.npx react-native start. The new debugger is the default in React Native 0.76 and later. For React Native 0.73–0.75, use the --experimental-debugger flag. Debugging works with OTA-deployed bundles as long as source maps are available at the expected URL.libhermes.so and is loaded at app startup through JNI. This introduces a small amount of additional initialization overhead compared to iOS.hermesEnabled=false in gradle.properties. However, Hermes is strongly recommended for most applications.npx react-native start) or Chrome DevTools through chrome://inspect. Note that Flipper was removed from React Native in version 0.74 and later, so React Native DevTools is now the recommended debugging solution. OTA-deployed bundles remain debuggable if the corresponding source maps are served alongside the bundle.Hermes bytecode source maps are a two-stage problem. Here's why your crash reports show incorrect line numbers and how to fix them.
Metro source map
Hermes source map
Combined map
The Hermes debugger (via Chrome DevTools or React Native DevTools) can inspect OTA-deployed bundles, but only if the source maps are accessible. For local debugging, serve the composed source map from your Metro development server. For production debugging, upload the composed source map to your error-tracking service, such as Sentry, Bugsnag, or Datadog.
Tip: Starting with React Native 0.76, React Native DevTools is the default debugger, so no additional flags are required. For React Native 0.73–0.75, use the --experimental-debugger flag to enable it.
AppsOnAir CodePush automates the entire source map pipeline. When you run appsonair-codepush release-react, the CLI bundles your app with Metro, compiles it with hermesc, composes both source maps, and uploads the final composed source map alongside the bytecode bundle. As a result, crash reports map directly to your original TypeScript and JSX source files.
No manual source map configuration is required.
The foundation. How OTA works, bundle structure, signing, and delivery mechanics.
Bridgeless mode broke OTA reloads. Hermes interacts with this in Bridgeless setups.
Hermes-specific rollback considerations: bytecode cache cleanup, version pinning, and safe fallback.
Use Hermes. It produces smaller bundles (typically 30–40% smaller), starts faster, and is the default JavaScript engine in React Native. Starting with React Native 0.80, Hermes is the only supported engine on iOS. JSC bundles are larger plain-text JavaScript files that must be parsed on every cold start. The main reason to use JSC today is for legacy Android applications that have not yet migrated, though the migration process is generally straightforward.
Yes, but only if the OTA provider compiles the bundle to Hermes bytecode (.hbc) before uploading it. If you push plain JavaScript to a Hermes-enabled app, Hermes will compile it on the device during the first launch. While this still works, it increases startup time. AppsOnAir CodePush handles bytecode compilation automatically, so you do not need to run hermesc manually.
One of the most common causes is shipping plain JavaScript instead of precompiled bytecode to a Hermes-enabled app. Plain JavaScript bundles are typically 30–40% larger than Hermes bytecode. Verify that your OTA build pipeline runs hermesc after Metro bundling. You can confirm this by inspecting the file header: xxd -l 8 bundle.hbc should display the Hermes magic bytes (1f 3f b4 c2).
Hermes bytecode requires a two-stage source map process. Metro generates JavaScript source maps, and the Hermes compiler generates bytecode-to-JavaScript source maps. These must be combined using the compose-source-maps.js script included with React Native. Upload the composed source map to your error-tracking platform. AppsOnAir CodePush automates both source map composition and upload.
Yes. On iOS, Hermes is compiled as a static framework and, starting with React Native 0.80, is the only available JavaScript engine. On Android, Hermes is distributed as a shared library (.so) and requires additional JNI initialization during startup. Cache locations also differ: iOS typically uses Library/Caches/hermes/, while Android uses cache/hermes/. The bytecode format itself remains platform-independent.
The Hermes Inspector is available only in debug builds. For production OTA debugging, rely on crash-reporting platforms such as Sentry, Bugsnag, or Datadog with properly composed source maps uploaded. This allows crash reports to resolve to the original file names and line numbers, even when the application is running Hermes bytecode. For on-device debugging of OTA updates, you can create a debug-signed build with OTA updates enabled.