React Native New Architecture: Fabric and TurboModules Explained
AI generated
RN
native
React Native · JSI · Fabric · TurboModules
React Native New Architecture
Fabric and TurboModules without the bridge, explained

The New Architecture replaces the old asynchronous bridge and its JSON serialization with JSI, a direct C++ interface between JavaScript and native code. Fabric takes over as the new renderer for synchronous layout, TurboModules load native modules lazily instead of eagerly at app startup, and Codegen generates type-safe interfaces for both platforms from a single TypeScript spec file.

19 min read JSI · Fabric · TurboModules · Codegen · Migration React Native 0.74+ · newArchEnabled

1. The old bridge: why JSON serialization became a bottleneck

In the classic React Native architecture, the JavaScript thread, the native UI thread, and a separate shadow thread ran strictly apart from each other. Communication between them happened exclusively through what is known as the bridge: an asynchronous queue through which every call, every event, and every layout change was serialized as a JSON string, collected into batches, and delivered to the other side at the end of a frame. This separation sounded elegant at first, since it shielded JavaScript from direct access to native threads. In practice, it created measurable overhead, because every object first had to be turned into a string and then parsed back out on the other side before anything could actually happen.

The real problem was not just the serialization itself, but the enforced asynchrony. If a component needed the actual height of a native view before starting an animation, it had to wait for a response over the bridge that would only arrive in the next frame. Synchronous native calls were architecturally impossible, because the bridge could not guarantee a reply within the same tick. In complex lists, gestures, or animations, these waiting times added up to noticeable stutter, which developers worked around with patterns like InteractionManager or manual batching instead of fixing the underlying problem.

On top of that, every native module was instantiated eagerly at app startup, regardless of whether the JavaScript side ever actually needed it. An app with forty linked libraries would initialize forty native modules on cold start, even if only five of them were needed on the current screen. It is exactly these three weaknesses, the JSON bridge, the forced asynchrony, and eager loading, that form the starting point for the New Architecture, which JSI, Fabric, and TurboModules address together.


// OLD ARCHITECTURE: everything crosses the async JSON bridge
import { NativeModules, findNodeHandle, UIManager } from 'react-native';

// A native call always returns via callback or promise, never synchronously
NativeModules.DeviceInfo.getBatteryLevel((error, level) => {
  if (error) {
    console.warn('Bridge call failed', error);
    return;
  }
  console.log('Battery level:', level);
});

// Measuring a native view requires a round trip through the bridge,
// the result only arrives in a later frame, never in the current tick
UIManager.measure(findNodeHandle(myViewRef.current), (x, y, width, height) => {
  // By the time this runs, the layout may already be stale
  console.log('Measured width:', width);
});

2. JSI: the JavaScript Interface as the foundation of the New Architecture

JSI, short for JavaScript Interface, is the central innovation on which the entire New Architecture is built. Instead of sending data through a serialized queue, JSI gives the JavaScript engine, whether Hermes, JavaScriptCore, or V8, direct access to C++ objects in memory. A so-called HostObject is presented to the JavaScript runtime as an ordinary JavaScript object, but its methods actually call C++ code without ever producing a JSON string. The call happens synchronously, on the same thread and in the same tick, just like a normal function call inside JavaScript.

This property is the decisive difference from the old bridge: where a promise or callback used to be required just to get any answer back from the native layer, code under the New Architecture can read a native value directly and synchronously, as long as the operation allows it. This opens up entirely new patterns, such as synchronously reading persisted storage before the first render, or directly measuring a view's layout without a frame delay. JSI is not a React Native specific construct, it is a general purpose C++ layer that other libraries, such as Reanimated, also use to build their own direct bindings into the JavaScript runtime.

It is important to note that JSI itself does not prescribe a threading model. Synchronous here means: no detour through a serialized queue, not necessarily the same thread for every operation. Computationally heavy native work can still be offloaded to a background thread, only the communication itself loses the bridge overhead. For developers, this means the New Architecture is not purely a performance feature, it fundamentally changes which interaction patterns are even possible between JavaScript and native code.

3. Fabric: the new renderer replacing UIManager and the old shadow tree

Fabric is the renderer of the New Architecture and replaces the old UIManager together with the classic shadow tree mechanism. In the old architecture, the shadow tree, which handles layout calculation with Yoga, existed as a separate structure on its own thread, and the results were then transferred asynchronously over the bridge to the UIManager on the native side. Fabric removes this separation by implementing the shadow tree in C++, so that both JavaScript and the native side can access it directly through JSI, without a bridge detour.

The practical effect: layout calculations can happen synchronously before the native view is actually drawn, which reduces flicker and layout jumps that could occur in the old architecture because of the asynchronous transfer. Fabric also allows a component to get direct access to a native view ref whose properties can be measured synchronously, instead of waiting for a later callback response. For libraries that interact closely with native views, such as camera components, maps, or video players, this means noticeably fewer race conditions between JavaScript state and the actual native rendering state.

Fabric also provides the technical foundation for React 18 features such as concurrent rendering and Suspense in React Native, because the C++ shadow tree can hold consistent, consistently committable states across multiple simultaneous render passes. Custom native UI components, however, need to be re-registered as Fabric components with a ComponentDescriptor for this to work, an old UIManager based custom view does not simply keep working under Fabric, it requires an adapted implementation on both platforms.

4. TurboModules: lazy loading instead of eager NativeModules

TurboModules are the direct successor to the old NativeModules API and solve the problem of eager loading, where every registered native module was instantiated at app startup, regardless of whether JavaScript ever called it. Under the New Architecture, a TurboModule is only actually instantiated once JavaScript accesses it for the first time, through a JSI mediated lazy loading mechanism. This noticeably reduces startup time, especially for apps with many third party libraries, since unused modules no longer block the critical path of a cold start.

The second major difference concerns type safety. Old NativeModules relied on loosely typed bridge calls, typos in method names or wrong argument types only surfaced at runtime, often with cryptic crash messages. TurboModules are instead defined through a TypeScript or Flow spec file, from which Codegen generates both the JavaScript interface and native interfaces for iOS and Android at build time. If the actual native implementation deviates from the spec, the build fails, not the app at runtime in front of the user.


// NativeDeviceInfo.ts — TurboModule spec file, read by Codegen at build time
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';

export interface Spec extends TurboModule {
  // Synchronous methods are explicitly marked, Codegen enforces this contract
  getBatteryLevelSync(): number;
  getDeviceModel(): Promise<string>;
  addListener(eventName: string): void;
  removeListeners(count: number): void;
}

export default TurboModuleRegistry.get<Spec>('DeviceInfo') as Spec | null;

5. Codegen: what actually gets generated from the TypeScript spec file

Codegen is the tool that translates the TypeScript spec file into actual, platform specific executable code, and that is exactly what makes TurboModules and Fabric components practically usable. From a single spec file, Codegen generates abstract HostObject classes on the C++ side, Objective-C++ protocols on the iOS side that a Swift class must implement, and Java or Kotlin interfaces on the Android side with exactly matching method signatures. The developer writes the actual logic only once per platform, Codegen handles the entire bridging boilerplate fully automatically.

For Codegen to find a spec file at all, the package must be referenced in the codegenConfig section of package.json, including which directories to scan and what the generated C++ module should be named. In monorepos or standalone native modules, Codegen typically runs automatically during pod install on iOS or during the Gradle build on Android, the generated result ends up in a build directory and is not manually versioned. If the spec file changes, for instance through a new method, all that is needed is a rebuild, no manual bridging file has to be adjusted by hand.


{
  "name": "react-native-device-info-turbo",
  "version": "1.0.0",
  "codegenConfig": {
    "name": "DeviceInfoSpec",
    "type": "modules",
    "jsSrcsDir": "src",
    "android": {
      "javaPackageName": "com.mironsoft.deviceinfo"
    },
    "ios": {
      "componentProvider": {}
    }
  },
  "peerDependencies": {
    "react-native": ">=0.74.0"
  }
}

6. Native bindings: implementing a TurboModule in Swift

Once Codegen has generated the matching Objective-C++ protocol from the spec file, a concrete Swift class on the iOS side must implement that protocol. The decisive difference from the old NativeModules API: the method signatures are now enforced by the compiler, not merely connected by convention. If a method is missing or a return type does not match the spec, the compilation already fails, long before the code ever runs on a real device.

On the Android side, the same principle runs through a Codegen generated Java interface implemented by a Kotlin class, including a TurboReactPackage entry that tells the framework how the module should be lazily instantiated. In both cases, the actual business logic remains platform specific Swift or Kotlin code, only the connection to JavaScript now runs through the generated, type-safe JSI layer instead of manually maintained bridge methods with RCT_EXPORT_METHOD macros.


// DeviceInfoModule.swift — implements the Codegen-generated protocol
import Foundation

@objc(DeviceInfoModule)
class DeviceInfoModule: NSObject, NativeDeviceInfoSpec {

  // Synchronous method, called directly through JSI, no bridge round trip
  @objc func getBatteryLevelSync() -> NSNumber {
    UIDevice.current.isBatteryMonitoringEnabled = true
    return NSNumber(value: UIDevice.current.batteryLevel)
  }

  @objc func getDeviceModel(_ resolve: @escaping RCTPromiseResolveBlock,
                             reject: @escaping RCTPromiseRejectBlock) {
    resolve(UIDevice.current.model)
  }

  @objc func addListener(_ eventName: String) {
    // Required by the Spec even if not used for this module
  }

  @objc func removeListeners(_ count: NSNumber) {
    // Required by the Spec even if not used for this module
  }
}

7. Migration path: enabling newArchEnabled in an existing app

Migrating an established app to the New Architecture does not start by flipping a flag, it starts with taking stock of every third party library in use. For each dependency, it needs to be clarified whether it already supports TurboModules and Fabric, whether it only works through the old bridge, or whether it uses an interop layer that supports both architectures at once. The React Native Directory project lists this status for most popular packages, which saves manually digging through individual issue trackers.

Once compatibility is clear, the technical activation follows. On Android, setting newArchEnabled=true in gradle.properties is usually enough, on iOS the RCT_NEW_ARCH_ENABLED environment variable is set when running pod install, which makes CocoaPods select the Fabric and TurboModule capable variants of the podspecs. After activation, testing should first happen in a development environment with a full reset of the Metro bundler caches, since old native artifacts compiled under the classic architecture can otherwise lead to confusing runtime errors.

A realistic migration plan proceeds step by step: first activation in a feature branch, then a full regression test of the most critical user flows, in particular lists with many items, gestures, camera and map integrations, and all custom native modules. Only once these areas run stably is the New Architecture rolled out in a staging build to a wider internal test team, before it goes live in production for all users.


#!/usr/bin/env bash
# Enable the New Architecture on both platforms and rebuild native projects

# Android: set the flag in gradle.properties
echo "newArchEnabled=true" >> android/gradle.properties

# iOS: install pods with the New Architecture flag set
cd ios
RCT_NEW_ARCH_ENABLED=1 pod install
cd ..

# Clear stale caches from the old architecture before the first run
watchman watch-del-all
rm -rf $TMPDIR/metro-* $TMPDIR/react-*
npx react-native start --reset-cache

8. Common breakage points when migrating to the New Architecture

The most common source of errors is custom native UI components that still access the old UIManager directly. Under Fabric, this API no longer exists in its previous form, a custom view must instead be registered as a Fabric component with its own ComponentDescriptor. If this step is skipped, the component either fails to render at all or throws a runtime error that at first glance seems unrelated to the actual cause.

A second common pitfall involves view flattening: Fabric optimizes the native view hierarchy more aggressively than the old renderer, which means refs to nested views may return a different native object than expected. Code that relies on a specific view hierarchy depth, for instance for manual measuring or animations outside of Reanimated, needs to be adjusted accordingly. NativeEventEmitter patterns from older libraries that still send events over the classic bridge queue also often only work under the New Architecture through a compatibility layer with noticeable extra overhead.

Third, timing sensitive code deserves a close look: because Fabric delivers layout measurements synchronously instead of through a later bridge callback, effects that used to intentionally rely on the delay of the old architecture can now fire too early or in a different order under the New Architecture. These issues usually only surface through manually clicking through critical screens, automated tests rarely fully catch pure timing regressions.

9. Old Architecture versus New Architecture side by side

The following table lays out the key differences between the classic bridge architecture and the New Architecture with JSI, Fabric, and TurboModules. It shows why the New Architecture is not just a marketing label, each of the three components addresses a concrete problem that was structurally unsolvable in the old architecture.

Dimension Old Architecture New Architecture
Communication layer Bridge with JSON serialization JSI: direct C++ bindings
Native calls async only, via callback/promise synchronous where it makes sense
Module loading eager, all modules at startup lazy via TurboModules
Renderer UIManager + asynchronous shadow tree Fabric with C++ shadow tree
Type safety manual bridging declarations Codegen from TypeScript spec

The comparison makes clear that the New Architecture is less a single feature and more a connected system. JSI provides the foundation, Fabric builds on it for the renderer, TurboModules build on it for native modules, and Codegen ensures that both sides honor the same, build-time checked contract. Anyone looking at only one of the three components in isolation underestimates how tightly Fabric and TurboModules are technically built on top of JSI.

Mironsoft

React Native apps for Magento stores, connected headless via REST and GraphQL

Ready for the New Architecture, or still on the old bridge?

We build performant React Native storefronts that talk to your Magento store through REST and GraphQL APIs, and we guide existing apps through the migration to Fabric and TurboModules, from library analysis to a stable rollout.

Architecture audit

Check library compatibility, identify Fabric and TurboModule gaps

Migration support

Enable newArchEnabled step by step, regression testing, stable staging rollout

Native module development

Build TurboModules with Codegen specs in Swift and Kotlin for Magento integrations

10. Summary

The New Architecture in React Native solves three concrete, structural problems from the old bridge era: JSI replaces JSON serialization with direct, synchronous C++ bindings between JavaScript and native code. Fabric replaces UIManager and the old asynchronous shadow tree with a shared, C++ implemented renderer that calculates layout synchronously and interacts more closely with native views. TurboModules replace NativeModules with lazily loaded, Codegen generated type-safe interfaces that surface errors at build time instead of in front of the user.

For existing apps, the migration is not a one click switch, it is a process: check library compatibility, enable newArchEnabled step by step in development and staging environments, specifically test critical screens with lists, gestures, and custom native modules. Anyone who goes through this path carefully benefits from noticeably shorter startup times, more robust layout timing, and a type system that catches native bugs before the app ever launches.

New Architecture, Fabric, and TurboModules — the essentials at a glance

JSI instead of the bridge

Direct C++ bindings replace JSON serialization, synchronous native calls become technically possible.

Fabric instead of UIManager

A C++ shadow tree calculates layout synchronously, closer interop with native views, foundation for concurrent rendering.

TurboModules instead of NativeModules

Lazy loading on first JS access instead of eager instantiation of every module at app startup.

Codegen from TypeScript spec

One spec file generates C++, Swift, and Kotlin interfaces, contract breaks surface at build time, not in front of the user.

11. FAQ: New Architecture, Fabric, and TurboModules

1What exactly is the New Architecture?
The combination of JSI, Fabric, and TurboModules, which replaces the old asynchronous bridge and its JSON serialization with direct, synchronous C++ bindings.
2What does JSI actually replace?
The classic bridge queue with JSON serialization. JSI gives the JS engine direct access to C++ objects in memory.
3What does Fabric do differently from UIManager?
Fabric implements the shadow tree in C++ and shares it via JSI. Layout is calculated synchronously instead of asynchronously over the bridge.
4What are TurboModules?
The successor to NativeModules, lazily instantiated on first JS access, with a type-safe interface generated by Codegen.
5What does Codegen generate from a spec file?
C++ HostObject classes, an Objective-C++ protocol for iOS, and a Java/Kotlin interface for Android, with exactly matching signatures.
6How do I enable newArchEnabled?
Android: newArchEnabled=true in gradle.properties. iOS: RCT_NEW_ARCH_ENABLED=1 when running pod install. Check library compatibility first.
7Which libraries cause the most problems?
Custom native UI views with direct UIManager access and libraries with outdated NativeEventEmitter patterns.
8Are synchronous calls always a good idea?
No, only where it fits. Heavy computation should still run asynchronously to avoid blocking the JS thread.
9Do I have to rewrite my native modules?
Logic usually stays, but the JS connection must switch to the Codegen spec, and custom views need a ComponentDescriptor.
10Is the New Architecture already the default?
Yes, enabled by default for new projects since React Native 0.76, existing apps still need a planned migration path.