React Native – Internals: Hermes, JSI and the New Architecture

August 18, 20264 min readUpdated 8/24/2026

You can ship a React Native app without knowing any of this. You cannot debug a strange performance problem without it, and the vocabulary turns up in every release note and every interview.

The shape of it

Three things are running.

A JavaScript engine executes your code — your components, your state, your effects. It is Hermes, built by Meta for this job.

The native side owns the actual UI. Real UIViews on iOS, real Views on Android, laid out and drawn by the platform.

A layout engine, Yoga, computes flexbox. It is written in C++ and shared by both platforms, which is why your layout behaves identically on each — and why the flexbox defaults differ from the web's: Yoga is not a browser and made its own choices.

React reconciles as it always does. What changes is the renderer: instead of producing DOM mutations it produces instructions for native views.

The old bridge, and why it was replaced

Originally the two sides communicated over an asynchronous bridge. Every call was serialised to JSON, queued, and delivered in a batch. JavaScript could not call a native function and get an answer — it sent a message and something happened later.

That design was the source of the era's characteristic problems. Serialising everything is expensive at volume, which is why a scroll handler driven from JavaScript stuttered. Nothing was synchronous, so measuring a view meant a round trip. And startup was slow because every native module was initialised whether you used it or not.

JSI

The JavaScript Interface replaced it, and it is the foundation of everything else. JSI is a lightweight C++ layer that lets JavaScript hold a reference to a C++ object and call its methods directly — synchronously, with no serialisation.

Two consequences worth understanding. Calls can be synchronous when they need to be. And the engine is now swappable: because the interface is C++ rather than baked into one runtime, Hermes replaced JavaScriptCore without rewriting the world.

The New Architecture

Three pieces on top of JSI, all enabled by default now.

Fabric is the new renderer. It keeps an immutable C++ tree of your UI shared by both sides, so layout can be computed on any thread and a view's dimensions can be read synchronously. In practice: fewer frames where a view is briefly the wrong size, and no round trip to measure something.

TurboModules are lazily initialised native modules. Under the old system every module loaded at startup; now a module loads the first time you touch it, which is most of the startup win.

Codegen generates the C++ glue between your TypeScript types and the native interfaces at build time, so a mismatch is a compile error instead of an undefined at runtime.

What this means for ordinary code is: almost nothing. You write the same components. It matters when you write a native module, when you read a library's changelog, and when you wonder why an app feels faster than it used to.

Hermes

Hermes is optimised for the thing a mobile app actually does — start quickly on a device with a cold cache — rather than for peak throughput on a long-running benchmark.

The main trick is ahead-of-time bytecode. Your JavaScript is compiled to Hermes bytecode during the build, so the app starts by loading bytecode rather than parsing source. On a large bundle that is a large fraction of startup time.

It also uses less memory and produces a smaller download than shipping JavaScriptCore.

The practical consequences show up in ordinary code. Hermes ships a full ICU-backed Intl, so date and currency formatting work as they do in a browser — but that was not always true, which is why careful code still guards it. Some very new language features arrive later than in V8. And there is no Web Crypto: crypto.randomUUID() is simply undefined, which is why the demo app uses expo-crypto rather than reaching for Math.random().

Two threads, and where your code is

This is the practical model to keep in your head.

The JavaScript thread runs your components, effects and handlers. The UI thread lays out and draws. If the JavaScript thread is busy, the app is not necessarily janky — a scroll driven natively stays smooth — but anything waiting on your code is stalled.

That single fact explains several rules from earlier lessons. useNativeDriver: true hands an animation to the UI thread so a busy JavaScript thread cannot stutter it. Reanimated goes further and runs your animation logic there. And the dev menu's performance monitor shows both frame rates separately for exactly this reason: which one is dropping tells you where to look.

How your code gets to the device

Metro is the bundler. It resolves modules — including the .ios.tsx/.android.tsx/.web.tsx platform extensions — transforms them with Babel, and produces one bundle per platform. In development it serves that bundle over HTTP, which is what makes fast refresh work and why a physical device needs to reach your machine over the network.

In a release build the bundle is compiled to bytecode and embedded in the binary. That is why a production build behaves differently enough that profiling a debug build tells you very little.

What is next

Building and Releasing to the App Stores — the last mile, and the part web developers underestimate.