Dissecting the Hermes build pipeline in React Native: how the Metro bundler turns source into a bundle, the role of HermesBytecodePlugin which compiles the bundle into HBC bytecode, the bundle-command and hermesc options for full control, up to producing the artifact output, source maps, and debugging symbols.

In episode 3, Hermes is already active in your project. Now let's look at what actually happens when you run the build command. That's where Hermes meets the Metro bundler — and where bytecode is born.
Episode 4 dissects the build pipeline: Metro's integration with HermesBytecodePlugin, the bundle-command and hermesc options, and how to read the artifact output, source maps, and debugging symbols.
A React Native bundle goes through two stages. In the first stage, Metro reads all JavaScript modules and combines them into a single bundle file. In the second stage, hermesc compiles that bundle into HBC bytecode.
source modules --> Metro bundler --> bundle.js
|
v
HermesBytecodePlugin
|
v
bundle.hbc (+ .map)HermesBytecodePlugin is the bridge between the two — it invokes hermesc at the end of the bundling process. In modern React Native, this plugin is installed automatically by the built-in Metro config.
Metro uses the metro.config.js file to manage transforms and bundling. In React Native with Hermes, the default config already includes HermesBytecodePlugin through the getDefaultConfig helper. An example minimum config:
const { getDefaultConfig } = require("@react-native/metro-config");
const config = getDefaultConfig(__dirname);
module.exports = config;Behind the scenes, this helper registers HermesBytecodePlugin as a bundle plugin — a hook that Metro runs after the JavaScript bundle is produced. The final result is a bytecode file that React Native loads at runtime.
In detail, this plugin does two things:
hermesc with flags already calibrated for React Native..hbc, not .js.If you want to see this plugin explicitly in the config, use HermesBytecodePlugin directly from the React Native package:
const { getDefaultConfig } = require("@react-native/metro-config");
const HermesBytecodePlugin = require("react-native/HermesBytecodePlugin");
const config = getDefaultConfig(__dirname);
config.transformer.bundlePlugins = [HermesBytecodePlugin];
module.exports = config;This plugin reads configuration such as the compiler options set in React Native's package.json file. For full control over the compiler, you can override them with the options we cover next.
The most flexible way to control the pipeline is bundle-command — an option that tells React Native to call a custom command instead of the default. This is useful for build pipelines in CI or bytecode experiments. Example usage:
npx react-native bundle \
--platform android \
--dev false \
--entry-file index.js \
--bundle-output dist/index.android.bundleThis command still produces a regular JavaScript bundle; the bytecode compilation is still done by the built-in plugin. bundle-command is useful when you want to add a post-processing step after Metro finishes — for example, measuring the bundle size or signing it.
For experiments, call hermesc directly (the binary from episode 3) without Metro at all:
build/bin/hermesc \
-O \
-emit-binary \
-output-source-map \
-bundle \
-w=-0 \
index.js \
-out dist/index.android.bundle.hbcFlag explanations:
-emit-binary — produces a .hbc bytecode file.-bundle — marks that the input is a Metro bundle, not a single module.-output-source-map — produces a source map for debugging.-w=-0 — disables warnings that are really just warnings.-O — enables the optimizer; full details in episode 5.After the build, inspect what's produced in the android/app/build/generated/assets folder (Android) or in the bundle output directory:
dist/
index.android.bundle <- JS bundle (Metro)
index.android.bundle.hbc <- Hermes bytecode (hermesc output)
index.android.bundle.map <- source mapThe .hbc file is what finally gets embedded into the APK. It's usually smaller than the JS bundle because bytecode is denser — and this is what we'll optimize in episode 5.
A source map connects bytecode with the original source code, so the stack traces reported by the app can be mapped back to the actual files and lines. Hermes produces a source map via the -output-source-map flag above. In React Native, a source map is produced automatically during a release build:
npx react-native run-android --mode releaseThe release source map is generated in the project's dist folder. Without a source map, production errors only show bytecode positions that humans can't read — a problem we solve in episode 10.
Warning
Source maps contain mappings to the original source. For production apps, make sure the source map doesn't get embedded in the APK — store it on a backend symbol server and keep it from leaking, because it could expose your source code.
Besides source maps, Hermes provides debugging symbols for tools like Android Studio. During a release build, React Native generates a map file with hermesc containing symbol information for native stack traces. To download symbols on a crash, SDK tools usually look for the .symbol file produced by -output-source-map together with -emit-binary. This is what crash reporting services use to translate Hermes crashes back to their original source locations.
The Hermes build pipeline is now mapped out. Here's what you must take away:
HermesBytecodePlugin compiles it into .hbc.HermesBytecodePlugin is installed automatically in modern React Native; it can also be installed explicitly in metro.config.js.bundle-command and direct hermesc invocation give you full control over the pipeline.Next, in episode 5 we optimize: bytecode prepackaging, the -O optimizer flag, and how bundle size affects cold start. See you there!