A recall reference for engineers who already ship RN. Covers the legacy Bridge architecture and the New Architecture (JSI · Fabric · TurboModules · Codegen · Bridgeless). The goal: rebuild the whole picture in your head in 5 minutes.
Your JS runs in a JS engine (Hermes) on its own thread. It never touches UIKit/Android views directly. In the old world, JS talked to native through an async, JSON-serialized message queue (the Bridge) — one lane, batched, no return values. In the new world, JSI gives JS engine-agnostic C++ bindings so JS can hold references to native objects and call them synchronously with zero serialization. On top of JSI sit TurboModules (native APIs) and Fabric (the renderer, with its shadow tree in C++). Codegen generates the type-safe glue from your TS specs. Bridgeless mode deletes the message queue entirely. Layout is always Yoga (Flexbox in C++).
Before flows and threads, know the actors. Colors below: JS Platform native C++ core New Arch
| Piece | What it is | Where |
|---|---|---|
| Metro | The JS bundler/dev server. Transforms + bundles your code into one .jsbundle (or serves it over HTTP in dev). Also does Fast Refresh. | JS build |
| JS Engine | Runs your JavaScript. Default is Hermes; can be JSC or V8. Executes React + your app logic. | JS |
| React | Same React you know — reconciler + hooks. RN just swaps the host config: instead of DOM nodes it emits native view instructions. | JS |
| Yoga | Cross-platform Flexbox layout engine in C++. Turns style props into x/y/w/h. Used by both architectures. | C++ |
| Bridge (legacy) | Async, batched, JSON-serialized message queue between JS and native. The old single point of contact. | native |
| JSI new | C++ interface the JS engine exposes. Lets JS hold HostObjects and call C++ directly & synchronously. Engine-agnostic. The foundation everything new sits on. | C++ |
| TurboModules new | Native modules on JSI. Lazy-loaded, type-safe, can be synchronous. Replaces legacy NativeModules. | C++ + native |
| Fabric new | The new renderer. Shadow tree + much logic moved to shared C++, called directly through JSI. Enables concurrent React, sync layout, better view flattening. | C++ |
| Codegen new | Build-time tool. Reads your TS/Flow spec files and generates the C++/ObjC/Java interfaces so the boundary is type-checked. | build |
This is the thing to burn into memory — most RN performance intuition comes from knowing which thread does what.
measure during render).Old arch: crossing between JS and Main threads always went through the async Bridge. New arch: JSI lets a call originate on the JS thread and execute C++ synchronously — no thread hop, no queue, no JSON.
Everything JS ↔ native went over one asynchronous channel. Think of it as a message queue where both sides post batches.
CameraModule.takePhoto() or "create a <View>". This is just a JS function call into a proxy.Replace the serialized message queue with a direct, shared-memory C++ interface both sides can call into. That interface is JSI.
JavaScript Interface. A lightweight, general-purpose C++ API that a JS engine implements. It lets native C++ install objects/functions into the JS runtime, and lets JS hold references to and directly invoke C++ — no bridge, no JSON, optionally synchronous.
| Concept | Meaning |
|---|---|
jsi::HostObject | A C++ object exposed to JS. JS sees a normal object; property gets/sets and method calls are routed to your C++ (get()/set()). |
jsi::Function / HostFunction | A C++ function callable from JS (and vice-versa). Args arrive as jsi::Values — no serialization, they're views over engine values. |
// Conceptually: install a native-backed object into JS auto obj = jsi::Object::createFromHostObject(rt, myCppModule); rt.global().setProperty(rt, "__MyModule", obj); // JS side — this now calls straight into C++, synchronously: const ts = global.__MyModule.nowMillis(); // returns a value, no await
jsi::Values instead of stringifying everything.JSI is to RN what the DOM binding is to a browser: the thin C++ seam where "JS values" and "native objects" meet. TurboModules and Fabric are both just consumers of JSI.
The replacement for legacy NativeModules. Same idea (JS calling platform code) but built on JSI.
| Legacy NativeModule | TurboModule | |
|---|---|---|
| Transport | Async bridge + JSON | Direct via JSI |
| Loading | Eager (all at startup) | Lazy — created on first access |
| Sync calls | Impossible | Possible |
| Type safety | Manual, runtime | Codegen-enforced at build |
You define a spec (a typed interface). Codegen produces a native base class you implement in ObjC/Kotlin/C++. JS gets a JSI-backed proxy that resolves the actual module lazily on first call.
// NativeCalc.ts — the spec Codegen reads import {TurboModule, TurboModuleRegistry} from 'react-native'; export interface Spec extends TurboModule { add(a: number, b: number): number; // note: sync return } export default TurboModuleRegistry.getEnforcing<Spec>('Calc');
The new rendering system. The big move: the shadow tree and rendering logic live in shared C++, called directly through JSI, instead of being coordinated over the async bridge via UIManager.
| Tree | What | Lives in |
|---|---|---|
| React Element Tree | What your components return each render. Immutable, recreated per render. | JS |
| React Shadow Tree | C++ mirror of the element tree. Each ShadowNode holds props + a Yoga node. This is where layout is computed. | C++ |
| Host View Tree | The real UIView / android.view.View objects on screen. | Main thread |
startTransition actually work because the renderer supports interruptible/prioritized rendering.Both TurboModules and Fabric components need the JS side and native side to agree on exact types. Writing that boilerplate by hand is error-prone, so Codegen generates it at build time from your specs.
Native*.ts, component configs)If you've written a New Arch native module and hit "missing generated header" errors — that's Codegen not having run. It's wired into the RN build (Gradle / pod install), keyed off the codegenConfig in package.json.
The New Arch can technically run with the old bridge still present (for interop). Bridgeless removes it entirely — no message queue anywhere. Communication is 100% JSI, and a new RuntimeScheduler handles prioritization/ordering of work that the bridge's batching used to (accidentally) provide.
New Architecture + Bridgeless is the default direction of modern RN. The bridge is legacy-compat, not the happy path.
What actually happens between setState and pixels changing.
UIView/View objects and set their frames. Screen updates.Events (touch, scroll) flow the other way: native captures them, and through the C++ event system + JSI they're delivered to JS handlers — with priorities via the RuntimeScheduler.
ReactHost/RCTHost) initializes.index.jsbundle (prod) or fetch from Metro (dev). With Hermes this is precompiled bytecode → no parse cost.AppRegistry.runApplication mounts your root component.The default JS engine, purpose-built for RN's constraints (mobile, short-lived, startup-sensitive).
Hermes exposes the Chrome DevTools / Hermes debugger protocol; Flipper and the newer RN DevTools hook into it. Source maps map bytecode traces back to your TS.
| Dimension | Legacy | New Architecture |
|---|---|---|
| JS ↔ native | Async Bridge, JSON | JSI, direct C++, sync-capable |
| Native APIs | NativeModules (eager) | TurboModules (lazy) |
| Renderer | Paper / UIManager | Fabric (C++ shadow tree) |
| Type safety | Runtime, manual | Codegen, build-time |
| Concurrent React | No | Yes |
| Sync layout/measure | No | Yes |
| Message queue | Always present | Removed (Bridgeless) |
| Engine coupling | Effectively JSC-era | Engine-agnostic (Hermes/JSC/V8) |
| Layout engine | Yoga (Flexbox, C++) — same in both | |