Demystifying React Native New Architecture: JSI, Fabric, and TurboModules

Demystifying React Native New Architecture: JSI, Fabric, TurboModules, and the Death of the Bridge

An exhaustive compiler-and-runtime masterclass on React Native's modern execution model — exploring the JavaScript Interface (JSI), Fabric's synchronous C++ renderer, Yoga layout engine internals, Codegen type safety, TurboModule lazy loading, and thread execution pipelines.

For years, performance was React Native's Achilles heel. Developers noticed dropped animation frames, blank scroll cells, and sluggish gesture responses. The culprit was not JavaScript execution speed, nor was it native rendering capability. The bottleneck was the communication layer connecting both worlds: the asynchronous JSON Bridge.

Under the old architecture, the JavaScript thread and the Native (UI) thread were completely isolated from each other. They could only communicate by serializing all arguments into JSON strings, placing those serialized packets onto an asynchronous message queue, waiting for them to be dequeued on the other side, and deserializing the data before acting on it. Every user interaction — a scroll, a tap, an animation frame — had to traverse this slow serialization highway.

React Native's New Architecture eliminates this bottleneck entirely. The JavaScript Interface (JSI) replaces the JSON bridge with direct C++ host object bindings, allowing the JavaScript engine to call native C++ methods synchronously without any serialization overhead. Built on top of JSI are two major pillars: Fabric (the new concurrent rendering system) and TurboModules (the new lazy-loaded native module system), both of which are type-safe thanks to build-time code generation via Codegen.

To write high-performance React Native applications today, you must understand how these systems work at the C++ and thread level, what changed from the old architecture, and how to design your components and native modules for the new execution model.


1. The Old Bridge: Anatomy of a Performance Bottleneck

To appreciate the New Architecture, we must dissect exactly what made the old Bridge slow. The old architecture ran on three separate threads:

  • JS Thread: Ran the JavaScript business logic, event handling, and React reconciliation.
  • Shadow Thread: A secondary C++ thread that ran the Yoga layout engine, computing positions and sizes from React element trees.
  • Main/UI Thread: Performed native view creation, property updates, and on-screen rendering.

These three threads communicated via two asynchronous bridges: a JS-to-Native bridge and a Native-to-JS bridge. Each bridge serialized data as JSON strings. The serialization roundtrip introduced three compounding latency problems:

  • Serialization Cost: Every argument — numbers, strings, arrays, objects — was converted to a JSON text representation before being sent. On large payloads like scroll position objects or gesture state blobs, this consumed measurable CPU cycles per frame.
  • Queue Pressure: The asynchronous message queue was FIFO (first-in, first-out). During rapid scrolling, scroll event packets stacked up in the queue. By the time the UI thread processed an older scroll position, the user had already moved their finger further, causing visible blank cells and lag.
  • No Prioritization: The old bridge treated all messages with equal priority. A low-priority analytics event could block a high-priority animation frame update, causing frame drops on UI-critical paths.

The mathematical consequence is straightforward. If the bridge introduces $\Delta t$ latency per roundtrip and a smooth scroll at 60 FPS demands a layout update every $\frac{1000}{60} \approx 16.67$ ms, any bridge delay exceeding this budget directly causes dropped frames:

$$ \text{Available Budget} = \frac{1000\text{ ms}}{60\text{ FPS}} = 16.67\text{ ms} $$ $$ \text{Frame Drop} \iff \Delta t_{\text{bridge}} > 16.67\text{ ms} $$

On complex screens with many components, bridge round trips could easily exceed 20–30 ms, causing the UI to drop to 30 FPS or lower.


2. The JavaScript Interface (JSI): Direct C++ Bindings

JSI is a compact, general-purpose C++ API that allows a JavaScript engine — such as Meta's Hermes or WebKit's JSC (JavaScriptCore) — to interact directly with C++ host objects, bypassing all serialization.

Under JSI, instead of sending a JSON string like {"method":"Camera","args":[640,480]} across a queue, the JavaScript engine holds a direct C++ reference to a Host Object. A Host Object is any C++ class that implements the jsi::HostObject interface by providing get() and set() methods. These methods are callable directly from JavaScript:

// C++ Host Object Definition
class NativeCalculatorHostObject : public jsi::HostObject {
public:
jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &name) override {
if (name.utf8(rt) == "add") {
return jsi::Function::createFromHostFunction(rt, name, 2,
[](jsi::Runtime &rt, const jsi::Value &thisVal,
const jsi::Value *args, size_t count) -> jsi::Value {
double a = args[0].asNumber();
double b = args[1].asNumber();
return jsi::Value(a + b); // Direct return, no JSON
});
}
return jsi::Value::undefined();
}
};

From JavaScript, calling this method requires no serialization whatsoever:

// JS Thread — direct C++ call, synchronous, no bridge
const calc = global.__nativeCalculator; // JSI host object reference
const result = calc.add(10, 20); // Executes C++ directly, returns 30
console.log(result); // 30

The jsi::Value type is a tagged union that can represent any JavaScript primitive — number, string, boolean, object, function — in a C++-native way. There is no JSON serialization, no memory copy, no async queue. The JS engine invokes C++ code on the same execution thread and receives a return value instantly.

JSI is also JavaScript engine–agnostic. Meta ships Hermes as the default engine for React Native because Hermes compiles JavaScript to bytecode at build time (Ahead-of-Time compilation), reducing startup parsing overhead significantly. JSC is still supported for cases where full JIT compilation is required. Both engines expose the same JSI C++ interface, so all TurboModules and Fabric components work identically regardless of which JS engine is running.


3. Fabric: The New Concurrent C++ Renderer

Fabric is the complete reimplementation of React Native's rendering pipeline, written in C++ and deeply integrated with JSI. It replaces the old Shadow Thread and the old UI Manager, providing both synchronous commit capability and true concurrency support.

3.1 The Rendering Pipeline

Fabric's rendering process flows through four stages:

graph LR A["React Element Tree (JS)"] --> B["Shadow Node Tree (C++)"] B --> C["Yoga Layout Pass"] C --> D["Committed View Tree (Native UI)"]

*Mermaid Diagram: Fabric's four-stage rendering pipeline from React elements to committed native views.

  • React Element Tree: The immutable tree of React elements produced by the React reconciler on the JS thread. Each element describes a UI component's type, props, and children.
  • Shadow Node Tree: A C++ representation of the same tree structure built directly via JSI. Each Shadow Node holds layout constraints, style properties, and measurement callbacks. Crucially, this tree is immutable and thread-safe.
  • Yoga Layout Pass: The open-source Yoga layout engine (written in C++) traverses the Shadow Node tree to compute absolute positions and dimensions for each node. Yoga implements a CSS Flexbox-compatible layout algorithm, so React Native components behave identically to web Flexbox.
  • Committed View Tree: After layout computation, Fabric commits the calculated positions to native platform views synchronously. On Android, this interfaces with the View Hierarchy via Android's View system. On iOS, this interfaces with UIKit's UIView hierarchy.

3.2 Synchronous Commit: Solving the Text Input Bug

One of the most infamous bugs under the old architecture was the text cursor jump bug. When a user typed in a React Native TextInput, the JS thread would receive the keystroke event, update component state, pass the new string back to the native TextInput via the async bridge, and the native field would then try to reconcile its current text with the incoming string. This async round trip caused the cursor to jump to the end of the field on every keystroke because by the time the state update arrived, the native component had already advanced the cursor position.

Under Fabric, the JS thread can call fabric.synchronouslyUpdateViewOnUIThread() via JSI. This function executes the Shadow Node tree diff and View commit synchronously on the UI thread, completing the entire update cycle before returning control to JavaScript. The text cursor position and native view state are always in sync because there is no asynchronous gap between state change and native commit.

3.3 Concurrent Mode and Priority Scheduling

Because Fabric uses immutable Shadow Node trees, multiple render passes can be prepared concurrently without locking. React 18's concurrent features (like startTransition) work with Fabric to deprioritize non-urgent renders. A high-priority user interaction (tap, scroll) can interrupt a low-priority background data update, ensuring responsive UI at all times.

The priority model follows React Scheduler's time-slicing algorithm: each render unit has an associated priority level $p \in \{1 \dots 5\}$, and the scheduler preempts low-priority tasks whenever a higher-priority input event arrives within a given time slice $\tau$:

$$ \text{Schedule}(task) = \text{min}_{p}(task \mid t_{\text{remaining}} > 0) $$

4. Yoga Layout Engine Internals

Yoga is the cross-platform C++ layout engine used by both the old Shadow Thread and the new Fabric renderer. Understanding its internals helps you optimize component trees for performance.

Yoga implements the W3C CSS Flexbox specification. When Fabric triggers a layout pass, Yoga traverses the Shadow Node tree in a depth-first manner, applying these rules:

  • Measure Phase: For leaf nodes (like Text or Image), Yoga calls the component's native measure function to get the intrinsic content size. For Text nodes, this invokes the platform's text layout engine (CoreText on iOS, StaticLayout on Android) to measure the rendered string dimensions.
  • Layout Phase: Starting from the root, Yoga computes the available main-axis and cross-axis space for each flex container and distributes it among children according to flex, flexGrow, flexShrink, and flexBasis properties. Final absolute pixel positions are computed for each node.
  • Dirty Flag Optimization: Yoga tracks a dirty flag per node. When a node's style or content changes, it marks itself and all ancestors dirty. On the next layout pass, Yoga only recomputes the subtree that contains dirty nodes, skipping clean branches. This avoids full-tree recalculations for small updates.

The computational complexity of a Yoga layout pass on a balanced tree of $n$ nodes is approximately $O(n)$ in the common case (no circular dependencies), because most nodes are visited exactly once during layout traversal.

$$ \text{Layout Time} \approx O(n) \text{ for acyclic flex trees} $$

5. TurboModules: Lazy-Loaded Native Modules via JSI

TurboModules are the replacement for the old Native Module system. The old system had a critical flaw: all native modules were eagerly initialized at application startup, regardless of whether they were needed. An app that used Camera in one specific flow still paid the initialization cost for Camera, Bluetooth, GPS, and every other native module at launch time.

5.1 Lazy Loading via the Module Registry

TurboModules are loaded on-demand. React Native maintains a TurboModule Registry (a C++ map from module name to factory function). When JavaScript first accesses a module, JSI calls the registry's getModule(name) method, which instantiates the C++ module object, injects it as a JSI Host Object into the JS environment, and returns a reference. Subsequent accesses reuse the cached instance.

// JS side — first access triggers module instantiation
import { NativeModules } from 'react-native';
const { CameraModule } = NativeModules;
// At this point, CameraModule C++ object is created and injected via JSI
const photo = await CameraModule.takePicture(options);

This lazy pattern means the startup time of an application using 20 native modules is no longer penalized by all 20 initializations. Only the modules actually accessed in the critical startup path are initialized early.

5.2 Codegen: Build-Time Type Safety

One of the most subtle but impactful improvements in TurboModules is Codegen. Under the old system, native module method signatures were defined in JavaScript at runtime via the getConstants() and method dictionary pattern. There was no compile-time guarantee that the JavaScript call signature matched the native implementation signature. A mismatch caused crashes only at runtime.

Codegen solves this by generating C++ bridge code at build time from typed JavaScript/TypeScript interface definitions. Developers write a TypeScript spec file:

// NativeCameraModuleSpec.ts
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';
export interface Spec extends TurboModule {
takePicture(options: {
width: number;
height: number;
quality: string;
}): Promise<string>; // Returns photo URI
getAvailableLenses(): string[];
}
export default TurboModuleRegistry.getEnforcing<Spec>('CameraModule');

The React Native Codegen tool parses this TypeScript interface and generates the following at build time:

  • C++ JSI Adapter: A C++ file that unpacks JSI values into strongly-typed C++ primitives and calls the native implementation. If options.width is missing, the generated adapter throws a typed error immediately.
  • Android Java/Kotlin Interface: An abstract Java/Kotlin class that the native Android module must implement. The compiler enforces the method signatures at build time.
  • iOS Objective-C/Swift Protocol: An ObjC protocol or Swift protocol that the iOS native module must conform to. The Xcode compiler enforces method signatures at build time.

This means type mismatches become build errors, not runtime crashes. The entire surface area of JS-to-native communication is type-checked by both the TypeScript compiler and the native platform compiler before the app ships.


6. Architecture Comparison: Threading Model

graph TD subgraph Old["Old Architecture"] JSO["JS Thread"] -- "JSON serialize" --> BridgeQueue["Async Bridge Queue"] BridgeQueue -- "JSON deserialize" --> ShadowT["Shadow Thread"] ShadowT --> UIT["UI Thread"] end subgraph New["New Architecture"] JSN["JS Thread"] -- "JSI direct call" --> FabricC["Fabric C++ Renderer"] FabricC -- "Synchronous commit" --> UIN["UI Thread"] end

*Mermaid Diagram: Old Bridge serialization pipeline vs New JSI direct-call model.

Feature Old Architecture New Architecture
Communication Async JSON queue (Bridge) Synchronous C++ direct call (JSI)
Rendering Shadow Thread + async commit Fabric C++ renderer, sync commit
Native Modules Eager initialization at startup Lazy loading via TurboModule registry
Type Safety Runtime only (crash on mismatch) Build-time Codegen enforcement
JS Engine JSC (JIT) Hermes (AoT bytecode) + JSC supported

7. Performance Benchmarks: Old Bridge vs New Architecture

The chart below compares measured FPS and startup time improvements achieved by migrating from the old Bridge to the new JSI / Fabric / TurboModules stack, benchmarked on a mid-range Android device running a complex scrollable list screen:

In practice, real-world improvements vary depending on the application's component complexity and bridge usage patterns. Applications with heavy JS-to-native callback density (e.g., gesture handlers firing 60 times per second) see the largest gains.


8. Migrating to the New Architecture

As of React Native 0.74+, the New Architecture is enabled by default for new projects. Existing projects can migrate incrementally. React Native provides a backward-compatibility interop layer that wraps old Bridge modules in a compatibility shim so they run alongside TurboModules without requiring immediate rewriting.

8.1 Enabling on iOS

# ios/Podfile
use_react_native!(
:path => config[:reactNativePath],
:fabric_enabled => true, # Enable Fabric renderer
:hermes_enabled => true # Enable Hermes JS engine
)

8.2 Enabling on Android

// android/gradle.properties
newArchEnabled=true
hermesEnabled=true

8.3 Common Migration Pitfalls

  • Synchronous Event Handlers: Some third-party libraries rely on synchronous JS callbacks from native events which were not guaranteed synchronous under the old Bridge. Under Fabric, synchronous callbacks must be explicitly declared in the Codegen spec.
  • NativeMethodsMixin: The old NativeMethodsMixin API for measuring and animating native views directly is being deprecated. Use the new useAnimatedRef and Reanimated v3 Worklets instead.
  • findNodeHandle: This API returns a numeric node handle used by old Bridge APIs. Under Fabric, views use a different internal identifier system. Use the ref prop and host component APIs instead.

9. Developer Common Pitfalls and Safety Guards

  • Assuming Bridge is Still Present: Many developers assume the Bridge still exists and try to call NativeModules in the old way. Under the New Architecture with strict mode enabled, unregistered modules throw immediately. Always use the TurboModuleRegistry pattern.
  • JSI Synchronous Calls Can Block the UI Thread: While JSI allows synchronous calls, calling a slow C++ method synchronously will freeze the JS thread for its entire duration. Use Promise-based async patterns for any I/O-bound native work.
  • Codegen Types Must Be Flow/TypeScript Compatible: Codegen parses Flow annotations or TypeScript annotations from spec files. Plain JavaScript without type annotations will fail code generation silently, producing undefined behavior.
  • Third-Party Library Compatibility: Not all third-party libraries support the New Architecture yet. Check the React Native Directory for the new-architecture badge before upgrading.

10. Interactive Thread Communication Simulator

Select the architecture mode, trigger a scroll event, and watch how each mode processes the event through its thread pipeline — observe serialization latency vs direct dispatch:

Rendered FPS
60
Thread Monitor
Ready. Click Trigger Scroll Event to begin.
JS Thread
Active
Comm Layer
Queue: 0
UI Thread
FPS: 60

11. Frequently Asked Questions (FAQ)

Q1: Can old native modules run on the New Architecture?

Yes. React Native includes a backward-compatibility interop layer that wraps old Bridge-based native modules so they can run alongside TurboModules. This allows incremental migration without requiring a complete rewrite of all native modules at once. The interop layer is a thin shim that routes JSI calls to the old Bridge dispatcher for unregistered modules.

Q2: What is Hermes and why is it preferred over JSC?

Hermes is Meta's open-source JavaScript engine optimized specifically for React Native. Its primary advantage is Ahead-of-Time (AoT) compilation: it compiles JavaScript source code to binary bytecode at build time (before shipping), eliminating the parsing and compilation step at app startup. This typically reduces time-to-interactive by 300–600ms on low-end Android devices compared to JSC's JIT compilation model.

Q3: How does JSI enable synchronous JS-to-native calls?

JSI exposes C++ Host Objects as first-class JavaScript values. When JS invokes a method on a Host Object, the JS engine's runtime calls the C++ function directly on the current execution thread. The call completes synchronously and returns a value to JS without touching any queue or serialization layer. However, developers must be careful: synchronous JSI calls block the calling thread until the C++ function returns, so slow I/O operations should still be wrapped in async Promises.

Q4: What does Codegen generate and where are the generated files?

Codegen generates three artifacts: (1) a C++ adapter file that unpacks JSI values into typed C++ structs and dispatches to the native implementation, (2) an Android Java/Kotlin abstract class defining the module's method contract, and (3) an iOS Objective-C protocol / Swift protocol. On iOS these appear in the build/generated/ios/ directory, and on Android in android/build/generated/ during Gradle build phases.

Q5: Is Reanimated v3 required for the New Architecture?

Reanimated v3+ is required for full New Architecture compatibility because it uses JSI worklets — JavaScript closures that execute directly on the UI thread via JSI, bypassing the React event system entirely. Old Reanimated v1/v2 relied on the Bridge for animation updates, which caused jank on complex animations. Worklets run at 60/120 FPS independently of the JS thread's load state.

Q6: What is the Yoga layout algorithm's time complexity?

For a typical acyclic tree of $n$ flex nodes, Yoga's layout pass runs in $O(n)$ time. Each node is visited once in the measure phase and once in the layout phase. The dirty-flag optimization further reduces this to $O(k)$ where $k$ is the number of dirty subtree nodes on incremental updates, making rapid state changes very efficient.

Q7: Does the New Architecture support React 18 Concurrent Features?

Yes. Fabric's immutable Shadow Node tree design is specifically built to support React 18 features like startTransition, useDeferredValue, and Suspense streaming. These features allow React to interrupt low-priority renders and prepare multiple versions of the UI tree concurrently, only committing the final result when all data is ready.

Q8: How do I check if a third-party library supports the New Architecture?

Visit reactnative.directory and filter by the "New Architecture" badge. Alternatively, check the library's package.json for a "react-native" exports condition and the presence of a codegenConfig field, which indicates Codegen spec files have been configured for TurboModule compatibility.

Post a Comment

Previous Post Next Post