why it makes apps noticeably faster
The Hermes engine is the JavaScript runtime React Native has used by default since version 0.70, built to start apps faster and run with less memory. Instead of re-parsing JavaScript on every app launch, Hermes precompiles the code into bytecode at build time, a change that benefits older Android devices with limited memory the most.
Table of Contents
- 1. Why React Native needed its own JS engine
- 2. AOT bytecode compilation: the heart of Hermes
- 3. App startup in practice: parse cost versus bytecode
- 4. Memory footprint and garbage collection
- 5. Enabling and verifying Hermes
- 6. JSI and the New Architecture
- 7. Debugging Hermes apps
- 8. Limitations of Hermes and a look at Static Hermes
- 9. Hermes compared: JSC, V8 and Hermes
- 10. Summary
- 11. FAQ
1. Why React Native needed its own JS engine
Until React Native 0.70, JavaScript ran by default on JavaScriptCore (JSC), the engine behind Safari. JSC is mature on desktop and iOS, but it was never optimized for the startup conditions of typical Android devices: limited memory, slower CPUs, and an app launch that requires parsing the entire JavaScript bundle every single time. Meta built the Hermes engine specifically for mobile devices with one clear goal: faster startup and lower memory usage, without requiring developers to change their code.
Since React Native 0.70, Hermes has been the default engine on both platforms, iOS included. That was a deliberate strategic shift: instead of using a general purpose engine built for every JavaScript environment, React Native gets a runtime tuned exactly to the constraints of mobile hardware. For teams targeting large user bases on older Android devices in growth markets, this is not a footnote, it is a decisive factor for conversion and app store ratings.
The core of the difference is not the language itself but how and when JavaScript gets turned into bytecode. That exact mechanism is the subject of the sections below, from compilation through startup and garbage collection to practical setup and the limits of the Hermes engine.
2. AOT bytecode compilation: the heart of Hermes
The central difference between the Hermes engine and classic JS engines is ahead-of-time (AOT) compilation. Instead of parsing JavaScript source at runtime and translating it into machine code via a just-in-time (JIT) compiler, Hermes compiles the JavaScript code into a compact bytecode format during the build process, using the `hermesc` tool. The shipped app no longer contains JS source text at all, only directly executable bytecode.
This approach shifts expensive work from the user's device to the developer's build pipeline. Parsing, syntax analysis, and most optimization decisions happen once at build time, not on every single app launch across potentially thousands of different devices. For an engine that has to serve millions of app launches on very heterogeneous hardware, this is a fundamental efficiency gain over classic JIT behavior.
The Metro bundler integration makes this step invisible to developers: during a production build, the JavaScript bundle is automatically run through `hermesc` and embedded in the app as a compiled bytecode file. Developers keep writing normal JavaScript or TypeScript, the conversion to bytecode is a build artifact that only appears at release build time.
# Hermes bytecode compiler runs automatically during release builds
# Manual invocation for inspection purposes:
node_modules/react-native/sdks/hermesc/osx-bin/hermesc \
--emit-binary \
-O \
-out index.android.bundle.hbc \
index.android.bundle
# Inspect bytecode size vs raw JS bundle size
ls -lh index.android.bundle index.android.bundle.hbc
3. App startup in practice: parse cost versus bytecode loading
To understand why the Hermes engine speeds up app startup, it helps to look at the classic JSC startup sequence: the engine loads the JS bundle as text, tokenizes it, builds a syntax tree, runs semantic checks, and only then begins execution. For larger bundles, common in production React Native apps with many screens and libraries, this parsing step makes up a noticeable share of overall Time to Interactive (TTI).
Hermes skips this step almost entirely. Since the bytecode is already available, the engine only needs to load and interpret it at startup, without first converting text into an intermediate representation. The result is a noticeably shorter gap between tapping the app icon and reaching an interactive first screen, especially on devices with slower CPUs, where parsing weighs proportionally the heaviest.
This effect is not evenly distributed across device classes: on powerful, current iPhones, the difference is often barely perceptible, because modern JSC JIT compilers parse very quickly there. On budget Android devices with little RAM and weak CPUs, common across many growth markets, the Hermes engine makes the biggest difference, often with noticeably shorter cold start times.
4. Memory footprint and garbage collection
Alongside startup, memory usage is the second major advantage of the Hermes engine. Hermes uses a generational garbage collector that groups objects into different memory areas (generations) based on their expected lifetime. Short-lived objects, the kind created on every React component render, are managed in a small, quickly searchable young memory area and efficiently reclaimed without scanning the entire heap.
This design reduces two concrete problems in the mobile context: out-of-memory crashes on devices with little RAM, and GC-related jank, perceived as visible stutter in the UI. Since mobile operating systems aggressively kill apps under memory pressure, a smaller, efficiently managed heap is directly tied to better app stability, not just abstract performance numbers.
In practice this shows up especially in list-heavy screens with many reused cells, such as social feeds or product catalogs. Without efficient garbage collection, fast scrolling accumulates many short-lived intermediate objects, whose cleanup on a less mobile-optimized engine can cause visible frame drops. Hermes' generational strategy keeps exactly these patterns performant.
5. Enabling and verifying Hermes
Since the Hermes engine has been the default since React Native 0.70, most new projects need no additional configuration. In older projects, or for manual control, the engine can be set explicitly through `gradle.properties` for Android and the Podfile for iOS. In Expo projects, a single entry in `app.json` is enough to set the engine per platform.
Verification after every build matters: a simple check against the global `HermesInternal` object reliably shows at runtime whether the app is actually running on Hermes or has silently fallen back to the classic engine, for example after a broken configuration change.
{
"expo": {
"name": "MyApp",
"jsEngine": "hermes",
"android": {
"jsEngine": "hermes"
},
"ios": {
"jsEngine": "hermes"
}
}
}
// Verify Hermes is actually active at runtime
import { useEffect } from 'react';
function EngineCheck() {
useEffect(() => {
// global.HermesInternal only exists when Hermes is the active engine
const isHermes = () => !!global.HermesInternal;
console.log('Running on Hermes:', isHermes());
}, []);
return null;
}
export default EngineCheck;
On the native side, it is worth checking `android/gradle.properties` (`hermesEnabled=true`) and the iOS Podfile (`:hermes_enabled => true`) if a project was migrated from an older React Native version. After any change to these files, a full clean build is required, since the engine choice is compiled into the native binary at build time and a simple JS reload is not enough.
6. JSI and the New Architecture
Another reason the Hermes engine is so central to React Native today is its tight integration with the JavaScript Interface (JSI). JSI replaces the old, asynchronous bridge, through which JavaScript and native code used to communicate exclusively via serialized JSON messages. With JSI, native objects can be held as direct references in JavaScript, enabling synchronous calls between both worlds without serialization overhead.
Hermes was designed with JSI compatibility from the start, making it ideally suited for TurboModules and the Fabric renderer of the New Architecture. While JSC had to be retrofitted for JSI support, the tight integration in Hermes is a native part of its design, which shows up as lower overhead on frequent cross-language calls.
For teams migrating to the New Architecture, the combination of Hermes and JSI is not an optional detail, it is the foundation on which TurboModules actually perform well. Anyone still on JSC loses part of the efficiency gains JSI is meant to provide, because the bridge between engine and native code requires more adaptation work there.
7. Debugging Hermes apps
Debugging with the Hermes engine works through the Chrome DevTools protocol, which Hermes natively supports. Developers can set breakpoints, inspect variables, and walk the call stack, similar to web development. The big difference from a classic remote debugging session: the JavaScript code keeps running directly on the device or simulator, not in a separate Chrome instance, which makes timing behavior more realistic.
For production crashes, symbolication via source maps is essential. Since shipped code exists as bytecode, stack traces without source maps are practically useless, showing only bytecode offsets instead of readable function names and line numbers. A common mistake in CI/CD pipelines is failing to consistently upload the source maps generated by `hermesc` to the crash reporting service, which renders production crash reports unusable.
A solid setup automatically uploads the source map file to the crash reporting tool on every release and links it to the corresponding build number. Without that step, even the most stable Hermes engine configuration stays hard to debug for real production errors, because the actual root cause remains hidden inside the bytecode.
8. Limitations of Hermes and a look at Static Hermes
The Hermes engine is not the superior choice in every scenario. Early versions were missing some `Intl` API functionality for internationalization, largely retrofitted since, but this can still matter on very old Hermes versions. Some debugging workflows, especially ones heavily reliant on specific JSC tooling, do not work identically with Hermes, even though the Chrome DevTools protocol covers most use cases.
The genuinely exciting development is Static Hermes, a project announced by Meta that precompiles JavaScript not just into bytecode but directly into native machine code. This would eliminate the runtime interpretation step entirely and reach performance characteristics close to classic JIT compilation, without its runtime overhead at app startup.
For most production apps today, the standard Hermes engine remains the right, well tested choice. Static Hermes is a glimpse of the next evolutionary step, but not yet a replacement for the currently shipped, stable bytecode variant running in millions of production React Native apps.
9. Hermes compared: JSC, V8 and Hermes
Choosing a JavaScript engine has direct consequences for startup time, memory usage, and debugging experience. The table below compares the three relevant engines in the mobile React Native context.
| Criterion | Hermes | JavaScriptCore (JSC) | V8 |
|---|---|---|---|
| Compilation model | AOT bytecode at build time | JIT at runtime | JIT at runtime |
| Startup (low-end Android) | Very fast | Slower due to parsing | Not used in production RN |
| Memory usage | Low, generational GC | Higher | Higher, tuned for desktop |
| JSI integration | Designed natively | Retrofitted | Not relevant to RN |
| Default in React Native | Yes, since RN 0.70 | Previous default | No |
The table makes clear why Meta chose Hermes as the default: the combination of AOT bytecode, lower memory usage, and native JSI support fits exactly what mobile apps need, while JSC plays to its strengths more in browser and desktop environments, the context it was originally built for.
Mironsoft
React Native performance audits and app optimization
Want professional startup and memory optimization?
We audit your React Native app's engine configuration, bytecode setup, and memory behavior, making sure the Hermes engine delivers its full performance potential, from the build pipeline to crash reporting.
Engine audit
Review of Hermes configuration, bytecode build, and source map pipeline
Startup optimization
Measuring Time to Interactive and improving it on low-end devices
Crash reporting
Source map upload and symbolication for reliable production diagnostics
10. Summary
The Hermes engine speeds up React Native apps because it moves the most expensive step of an app launch, parsing JavaScript, off the user's device and into the build pipeline. AOT bytecode compilation, a generational garbage collector, and native JSI integration together deliver shorter Time to Interactive, lower memory usage, and less GC-related jank, especially on low-end Android devices that make up the largest share of the global user base.
Practical setup is rarely a separate task anymore since React Native 0.70, because Hermes is the default. What still matters is consistent verification via `HermesInternal`, a working source map pipeline for crash reporting, and awareness of the few remaining limitations of the engine. Static Hermes already hints at the next evolutionary step, one that could eliminate the interpretation step entirely.
The Hermes Engine in React Native: Key Takeaways
AOT bytecode
JavaScript is compiled into bytecode with `hermesc` at build time, not parsed on every app launch.
Faster startup
Shorter Time to Interactive, most noticeable on low-end Android devices with weak CPUs.
Generational GC
Short-lived objects are managed efficiently, fewer OOM crashes and GC-related jank.
JSI & New Architecture
Native JSI integration is the foundation for performant TurboModules and the Fabric renderer.