Hermes · JSC · CodePush · Engine Guide · 2026

Your JS engine decides your OTA Configure it right

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.

Hermes
JSC
0110
.hbc Bytecode
40% Smaller
Bundle Pipeline

JSC Engine

app.js
Metro
.jsbundle
4.2 MB

Hermes Engine

app.js
Metro
hermesc
.hbc
2.5 MB

40% smaller OTA downloads

Pre-compiled bytecode skips on-device parsing

The Engines

Hermes vs JSC—what's different  for OTA

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.

JavaScriptCore (JSC)

Apple's Engine
Plain-text JS bundles: Metro outputs a single .jsbundle file. No compilation step. The engine parses and compiles JS on every cold start.
Larger bundle size: No bytecode pre-compilation means the full source text (minified) ships to the device. Typical production app: 3.5–5 MB.
Slower cold start: JSC must parse and JIT-compile the bundle on every app launch. Startup time scales with bundle size.
checked
Simple OTA: Bundle is just a text file; download, replace, reload. No cache invalidation needed. No bytecode version mismatches.
Removed from iOS in RN 0.80 and above. Android still supports it.

Hermes

Meta's Engine
Pre-compiled bytecode: The hermesc compiler transforms JavaScript into .hbc bytecode at build time. The engine loads the bytecode directly, with no parsing required.
30–40% smaller bundles: Bytecode is more compact than minified JavaScript. A typical production app bundle is around 2–3 MB. That can make a meaningful difference in OTA download times on 3G/4G networks.
Faster cold start: There is no parse step. Hermes memory-maps the bytecode file and begins execution immediately. Startup can be 2–4× faster than JSC for the same bundle.
OTA has gotchas: The bytecode cache must be invalidated after updates. Source maps require two-stage composition, and the bytecode version must match the Hermes engine version included in the binary.
Default engine on all platforms. Only engine on iOS from RN 0.80 and above.
Bytecode Deep Dive

How Hermes bytecode actually works

Understanding the compilation pipeline is key to debugging any Hermes OTA failure. Here's the full chain, from source code to execution.

01
Metro bundles your JS

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.

02
hermesc compiles to bytecode

The 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.

03
Hermes VM loads the .hbc file

At 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.

04
OTA replaces the bundle on disk

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.

The Bytecode Version Trap
If you compile your OTA bundle with Hermes v0.12 but the app binary contains Hermes v0.13, the VM will reject the bytecode with a Bytecode version mismatch error.. Always compile OTA bundles using the same hermesc version that was shipped with the app binary.
Real Benchmarks

Numbers from a production app

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.

Metric
Hermes (bytecode)
Hermes (plain JS)
JSC (plain JS)
Bundle Size
2.5 MB
4.2 MB
4.2 MB
Gzipped Size
0.8 MB
1.1 MB
1.1 MB
OTA Download (4G)
1.6s
2.8s
2.8s
OTA Download (3G)
4.8s
8.4s
8.4s
Cold Start (iOS)
340ms
820ms
1,420ms
Cold Start (Android)
480ms
1,040ms
1,850ms
Post-OTA Restart
340ms
820ms + compile
1,420ms
Memory at Startup
72 MB
98 MB
142 MB

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).

BUNDLE SIZE

JSC 4.2 MB
Hermes (JS) 4.2 MB
Hermes (HBC) 2.5 MB

COLD START (IOS)

JSC 1,420ms
Hermes (JS) 820ms
Hermes (HBC) 340ms

OTA DOWNLOAD (3G)

JSC 8.4s
Hermes (JS) 8.4s
Hermes (HBC) 4.8s
Configuration

Metro Configuration &  Hermes Build Pipeline

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.

01
metro.config.js
metro.config.js
const { getDefaultConfig, mergeConfig } = require(
  '@react-native/metro-config'
);

const defaultConfig = getDefaultConfig(__dirname);

/**
 * Metro configuration for Hermes + OTA
 *
 * Key points:
 * 1. Hermes bytecode compilation is handled by the
 * RN build pipeline (Xcode / Gradle), NOT Metro.
 * 2. For OTA, you need to compile bytecode yourself
 * using hermesc AFTER Metro bundles.
 * 3. Source maps need two-stage composition.
 */
const config = {
  transformer: {
    // Enable Hermes-compatible output
    hermesParser: true,

    // Minify for production OTA bundles
    minifierPath: 'metro-minify-terser',
    minifierConfig: {
      compress: {
        drop_console: true,   // Strip console.log
        dead_code: true,
        passes: 2,
      },
    },
  },

  serializer: {
    // Generate source maps for OTA debugging
    sourceMapUrl: 'index.map',

    // For Hermes: use "plain" output (not RAM bundles)
    // RAM bundles are incompatible with hermesc
    createModuleIdFactory:
      defaultConfig.serializer.createModuleIdFactory,
  },

  resolver: {
    // Ensure .hbc files aren't treated as assets
    assetExts: defaultConfig.resolver.assetExts.filter(
      (ext) => ext !== 'hbc'
    ),
    sourceExts: [
      ...defaultConfig.resolver.sourceExts,
      'cjs',
    ],
  },
};

module.exports = mergeConfig(defaultConfig, config);
02
OTA bundle build pipeline
Terminal
# Step 1: Bundle with Metro (plain JS + source map)
npx react-native bundle \
  --platform ios \
  --dev false \
  --entry-file index.js \
  --bundle-output ./build/index.bundle.js \
  --sourcemap-output ./build/index.bundle.js.map

# Step 2: Compile to Hermes bytecode
# Use the hermesc that ships with your RN version!
node_modules/react-native/sdks/hermesc/osx-bin/hermesc \
  -emit-binary \
  -O \
  -output-source-map \
  -out ./build/index.bundle.hbc \
  ./build/index.bundle.js

# Step 3: Compose source maps (bytecode → JS → source)
node_modules/react-native/scripts/compose-source-maps.js \
  ./build/index.bundle.js.map \
  ./build/index.bundle.hbc.map \
  -o ./build/index.bundle.composed.map

# Step 4: Upload to your OTA provider
# The bundle is .hbc, the map is .composed.map
AppsOnAir CodePush handles all of this
The AppsOnAir CLI detects your Hermes configuration, runs the correct 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.
Failure Catalog

Common Hermes OTA failures and fixes

Every Hermes OTA bug we've seen in production, with the exact error message and the fix.

Bytecode Version Mismatch

Error: Bytecode version mismatch
The .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.
Fix:

Always use the hermesc binary from the same node_modules/react-native version that was used to build the app binary.

Broken Source Maps

Symptom: crash reports show bytecode offsets, not file:line
Hermes bytecode requires composed source maps: a Metro source map (JavaScript → source) and a Hermes source map (bytecode → JavaScript). If you upload only one of them, your crash reports will show either bytecode offsets or bundled JavaScript line numbers, neither of which is useful for effective debugging.
Fix:

Use 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.

Pushing Plain JS to a Hermes App

Symptom: OTA works but cold start regresses 2–4x
If you push a plain .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.
Fix:

Always compile your bundle to .hbc before uploading it. Alternatively, use AppsOnAir CodePush, which automatically detects Hermes and performs the compilation for you.

RAM Bundle + Hermes

Error: crash on launch after OTA
Hermes does not support RAM bundles (inline requires). If your Metro config has serializer.getModulesRunBeforeMainModule or RAM bundle flags enabled, the output is incompatible with hermesc.
Fix:

Disable RAM bundles when using Hermes. Hermes bytecode already gives you the lazy-loading benefit that RAM bundles were designed for.

Failure Catalog

iOS vs Android—Hermes isn't identical fixes

Same engine, different platform integration. Here's what to watch for when deploying OTA bundles.

Static framework: Hermes is compiled as a static framework (hermes.xcframework). The engine is linked into the app binary at build time.
JSC removed in RN 0.80+: Starting with React Native 0.80, JSC is no longer available on iOS. Hermes is the only supported JavaScript engine, so there is no fallback option.
Hermes inspector: Connect using React Native DevTools with 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.
checked
App Review note: Apple does not restrict Hermes bytecode execution. Hermes bytecode is permitted under App Store Guideline 2.5.2.
Shared library: Hermes ships as libhermes.so and is loaded at app startup through JNI. This introduces a small amount of additional initialization overhead compared to iOS.
JSC still available: Android can still use JSC. To switch engines, set hermesEnabled=false in gradle.properties. However, Hermes is strongly recommended for most applications.
Hermes inspector: Connect using React Native DevTools (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.
checked
ProGuard note: Ensure ProGuard rules don't strip Hermes JNI bindings. The default RN template includes the correct rules, but custom ProGuard configs sometimes break this.
Source Maps & Debugging

Debugging OTA-deployed Hermes bundles

Hermes bytecode source maps are a two-stage problem. Here's why your crash reports show incorrect line numbers and how to fix them.

Stage 1

Metro source map

Stage 2

Hermes source map

Stage 3

Combined map

Hermes Inspector with OTA Bundles

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 Source Map Handling

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.

Go Deeper

Related technical guides

React Native OTA Updates Guide

The foundation. How OTA works, bundle structure, signing, and delivery mechanics.

New Architecture & OTA

Bridgeless mode broke OTA reloads. Hermes interacts with this in Bridgeless setups.

OTA Rollback Strategies

Hermes-specific rollback considerations: bytecode cache cleanup, version pinning, and safe fallback.

FAQ

Got Questions? We've Got Answers

Should I use Hermes or JSC for OTA updates?

Does Hermes bytecode work with CodePush OTA updates?

Why is my Hermes OTA bundle larger than expected?

How do I debug source maps for OTA-deployed Hermes bundles?

Are there differences between Hermes on iOS and Android for OTA?

Can I use the Hermes inspector on OTA-deployed bundles in production?