Mental Model · not the docs

React Native, Under the Hood

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.

TL;DR — the one-paragraph model

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

01 The pieces on the board

Before flows and threads, know the actors. Colors below: JS Platform native C++ core New Arch

PieceWhat it isWhere
MetroThe 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 EngineRuns your JavaScript. Default is Hermes; can be JSC or V8. Executes React + your app logic.JS
ReactSame React you know — reconciler + hooks. RN just swaps the host config: instead of DOM nodes it emits native view instructions.JS
YogaCross-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 newC++ interface the JS engine exposes. Lets JS hold HostObjects and call C++ directly & synchronously. Engine-agnostic. The foundation everything new sits on.C++
TurboModules newNative modules on JSI. Lazy-loaded, type-safe, can be synchronous. Replaces legacy NativeModules.C++ + native
Fabric newThe new renderer. Shadow tree + much logic moved to shared C++, called directly through JSI. Enables concurrent React, sync layout, better view flattening.C++
Codegen newBuild-time tool. Reads your TS/Flow spec files and generates the C++/ObjC/Java interfaces so the boundary is type-checked.build

02 The threading model

This is the thing to burn into memory — most RN performance intuition comes from knowing which thread does what.

JS Thread
Runs the JS engine: your app code, React reconciliation, event handlers, timers, business logic. Single-threaded. Block it (heavy JS loop) → dropped frames, unresponsive touches.
Main / UI Thread
The platform's UI thread (UIKit / Android UI). Creates & mutates the actual native views, handles gestures, drives native animations. Anything visible ultimately happens here.
Background / Shadow Thread layout
Where Yoga computes layout off the main thread. In legacy this is the "shadow thread." In Fabric, layout runs in C++ and can be invoked synchronously when needed (e.g. measure during render).
Key shift

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.

03 The legacy Bridge ≤ the old default

Everything JS ↔ native went over one asynchronous channel. Think of it as a message queue where both sides post batches.

JS thread
JS makes a call
e.g. CameraModule.takePhoto() or "create a <View>". This is just a JS function call into a proxy.
↓ serialize to JSON
Bridge
Message is queued & batched
Arguments are serialized to a JSON string. Calls accumulate and flush on a schedule — inherently asynchronous, no return value.
↓ deserialize
Main thread
Native side decodes & executes
Native module runs the work / UIManager creates the views. Any result comes back later as another async message (callback/promise).

Three "realms" it connected

04 Why the bridge had to die

Structural problems

  • Asynchronous only — JS can never synchronously read a native value. Everything is a round-trip.
  • Serialization tax — every call marshals to/from JSON. Big payloads (large lists, images-as-base64) choke the lane.
  • Single bottleneck — one queue for the whole app; congestion anywhere = jank everywhere.
  • Eager module init — all native modules initialized at startup, hurting TTI.

Symptoms you felt

  • Janky FlatList on fast scroll (event + view churn flooding the bridge).
  • Gesture/animation lag when JS thread is busy.
  • No true synchronous layout measurement.
  • Slow cold starts as module count grew.
  • No first-class support for React concurrent features.
The fix

Replace the serialized message queue with a direct, shared-memory C++ interface both sides can call into. That interface is JSI.

05 JSI — the foundation new arch

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.

The two primitives that matter

ConceptMeaning
jsi::HostObjectA 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 / HostFunctionA 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
Mental model

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.

06 TurboModules — native APIs on JSI new arch

The replacement for legacy NativeModules. Same idea (JS calling platform code) but built on JSI.

Legacy NativeModuleTurboModule
TransportAsync bridge + JSONDirect via JSI
LoadingEager (all at startup)Lazy — created on first access
Sync callsImpossiblePossible
Type safetyManual, runtimeCodegen-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');

07 Fabric — the renderer new arch

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.

Three trees to keep straight

TreeWhatLives in
React Element TreeWhat your components return each render. Immutable, recreated per render.JS
React Shadow TreeC++ mirror of the element tree. Each ShadowNode holds props + a Yoga node. This is where layout is computed.C++
Host View TreeThe real UIView / android.view.View objects on screen.Main thread

What Fabric buys you

08 Codegen — the type-safe glue new arch

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.

input
Spec files (Native*.ts, component configs)
Strictly-typed TS/Flow describing methods, props, events.
↓ codegen (build step)
output
C++ / ObjC++ / Java interfaces & prop structs
Abstract base classes you extend; prop parsers; event emitters — all type-checked against the spec.
Practical note

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.

09 Bridgeless mode new arch

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.

Where this lands

New Architecture + Bridgeless is the default direction of modern RN. The bridge is legacy-compat, not the happy path.

10 The Fabric render pipeline, end to end

What actually happens between setState and pixels changing.

JS thread · Render phase
1 · React reconciles
A state/prop change triggers a re-render. React diffs and, via the Fabric host config + JSI, creates/updates ShadowNodes (C++) — building a new React Shadow Tree.
↓
C++ · Commit phase
2 · Layout + commit
Yoga walks the new shadow tree and computes layout (positions/sizes). The new tree is then committed as the next tree to mount, diffed against the previous.
↓
Main thread · Mount phase
3 · Mount
The diff is applied to the real host views: create/remove/update 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.

11 Cold-start sequence

12 Hermes — the engine

The default JS engine, purpose-built for RN's constraints (mobile, short-lived, startup-sensitive).

Debugging note

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.

13 Old vs New — one-screen cheat sheet

DimensionLegacyNew Architecture
JS ↔ nativeAsync Bridge, JSONJSI, direct C++, sync-capable
Native APIsNativeModules (eager)TurboModules (lazy)
RendererPaper / UIManagerFabric (C++ shadow tree)
Type safetyRuntime, manualCodegen, build-time
Concurrent ReactNoYes
Sync layout/measureNoYes
Message queueAlways presentRemoved (Bridgeless)
Engine couplingEffectively JSC-eraEngine-agnostic (Hermes/JSC/V8)
Layout engineYoga (Flexbox, C++) — same in both

If you remember only 6 things

  • JS never touches native views directly — it always describes intent to a lower layer.
  • JSI is the whole new game: a C++ seam replacing a JSON message queue.
  • TurboModules = native APIs on JSI (lazy, sync-capable). Fabric = renderer on JSI (C++ shadow tree, concurrent React).
  • Codegen makes the boundary type-safe from your TS specs.
  • Pipeline = Render (JS) → Commit+Yoga layout (C++) → Mount (Main thread).
  • Three threads: JS, Main/UI, layout. Blocking the JS thread is still your #1 jank source.