The React Native New Architecture is a ground-up rewrite of the framework’s internals that removes the asynchronous, JSON-serialized bridge and replaces it with a set of C++ foundations: JSI, Fabric, TurboModules, and Codegen. JSI (JavaScript Interface) gives JavaScript a direct, synchronous, memory-sharing handle to native objects, eliminating the serialization overhead that caused dropped frames and race conditions. Fabric is the new rendering system that can measure and update views synchronously across threads, while TurboModules load native modules lazily and expose them through JSI instead of the bridge. Codegen ties everything together by generating type-safe native bindings from TypeScript specs. The New Architecture became the default in React Native 0.76, and by the 2025–2026 releases the legacy bridge is effectively deprecated. This guide explains each layer in code, shows how to enable and verify newArchEnabled, and covers the migration gotchas that actually break apps.
This post is a practical, senior-level tour. We will look at what the old bridge actually did, why JSI changes the game, how Fabric and TurboModules build on it, what Codegen generates for you, and — most importantly — the concrete things that break during migration and how to verify the whole thing is actually running.
Why Did the Old Bridge Have to Go?
In the legacy architecture, JavaScript and native code lived in two separate worlds that never shared memory. Every interaction — calling a native module, rendering a view, responding to a touch — was serialized to JSON, pushed onto a queue, and processed asynchronously on the other side. This design was elegant and portable, but it had three structural problems.
The bridge imposed costs that no amount of app-level optimization could remove:
- Everything was asynchronous. You could never read a native value synchronously; even a trivial getter round-tripped through the queue, so measuring a layout mid-gesture was impossible without hacks.
- Everything was serialized. Objects were stringified to JSON on one side and parsed on the other. Large payloads (image data, big lists, frequent scroll events) meant real CPU spent on serialization.
- The bridge was a single congested channel. A burst of messages — animations plus network callbacks plus layout — competed for the same queue, causing batching delays and visible jank.
The New Architecture attacks all three by giving JavaScript a direct, synchronous, memory-sharing handle to native code. That handle is JSI.
What Is JSI, the JavaScript Interface?
JSI (the JavaScript Interface) is a lightweight, engine-agnostic C++ API that lets native code hold references to JavaScript values and call JavaScript functions directly — and vice versa. Instead of serializing a message and posting it to a queue, native code installs C++ functions as properties on the JavaScript global object. When JS calls one of these, it is a direct C++ function invocation on the same thread, with no JSON in the middle.
Because JSI is engine-agnostic, React Native is no longer welded to JavaScriptCore. It runs on Hermes (the default), and could run on any engine that implements the JSI contract. The practical payoff is twofold: calls can be synchronous, and complex objects can be exposed as "host objects" backed by C++ memory rather than copied across a boundary.
Here is the shape of a JSI binding in C++. You rarely write this by hand today — Codegen does it — but seeing it demystifies the whole stack:
#include <jsi/jsi.h>
using namespace facebook::jsi;
// Install a synchronous native function onto the JS global object.
void installBindings(Runtime& runtime) {
auto multiply = Function::createFromHostFunction(
runtime,
PropNameID::forAscii(runtime, "multiply"),
2, // argument count
[](Runtime& rt, const Value& thisVal, const Value* args, size_t count) -> Value {
double a = args[0].asNumber();
double b = args[1].asNumber();
// Runs on the JS thread, returns immediately — no bridge, no JSON.
return Value(a * b);
});
runtime.global().setProperty(runtime, "__nativeMultiply", std::move(multiply));
}On the JavaScript side, __nativeMultiply(3, 4) now returns 12 synchronously. No promise, no callback, no serialization. This single capability — synchronous, zero-copy access to native — is the primitive that Fabric and TurboModules are built on.
JSI is not a feature you use directly. It is the foundation that makes Fabric and TurboModules possible: one shared C++ layer where JavaScript and native meet without a queue between them.
What Is the Fabric Renderer?
Fabric is the re-architected rendering layer. In the old world, React reconciled a tree in JavaScript, sent a stream of view-manipulation commands across the bridge, and native applied them asynchronously. Fabric replaces this with a C++ core (the "render core") that maintains an immutable shadow tree and can talk to both React and the native view layer through JSI.
How Fabric renders a frame
A Fabric render pass moves through three phases:
- Render: React runs your components and creates a React Element Tree, from which Fabric builds an immutable React Shadow Tree in C++.
- Commit: Fabric performs layout (Yoga runs in C++) and atomically promotes the new shadow tree to be the "next" tree to mount.
- Mount: Fabric diffs the previous and next trees and applies the minimal set of mutations to the actual native views.
Because the tree is immutable and shared via C++, Fabric unlocks capabilities the old renderer could not offer.
The concrete benefits of Fabric:
- Concurrent React support: Fabric is compatible with React 18+ features like useTransition, Suspense, and automatic batching, because it can prepare a tree off the main flow and commit it atomically.
- Synchronous layout access: measuring a node (for example, to position a tooltip) can happen synchronously through JSI, eliminating the extra frame of lag.
- Consistent multi-threaded rendering: layout runs off the JS thread, so heavy JS work is less likely to stall visual updates.
- Type-safe view props: props are generated by Codegen, so a mismatch between JS and native surfaces at build time rather than as a silent runtime no-op.
What Are TurboModules?
TurboModules are the successor to the old NativeModules. Two things change fundamentally. First, they are lazy: a module is only initialized the first time JavaScript actually accesses it, instead of every module being eagerly loaded at startup. For apps with dozens of native modules this measurably improves time-to-interactive. Second, because they are backed by JSI, their methods can be synchronous and skip serialization entirely.
You define a TurboModule by writing a typed spec. Codegen reads this spec and generates the native interfaces you implement. Here is a minimal spec for a device-info module:
// specs/NativeDeviceInfo.ts
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';
export interface Spec extends TurboModule {
// Synchronous — returns immediately, no Promise needed.
getDeviceName(): string;
getBatteryLevel(): number;
// Asynchronous work still returns a Promise.
isBiometricAvailable(): Promise<boolean>;
}
// TurboModuleRegistry.getEnforcing throws if the native side is missing,
// which surfaces linking mistakes early instead of returning undefined.
export default TurboModuleRegistry.getEnforcing<Spec>('NativeDeviceInfo');The Android implementation (Kotlin) extends the Codegen-generated abstract spec class. Note there is no bridge annotation and no promise juggling for the synchronous methods:
// android/src/main/java/com/deviceinfo/DeviceInfoModule.kt
package com.deviceinfo
import com.facebook.react.bridge.ReactApplicationContext
// NativeDeviceInfoSpec is generated by Codegen from your TS spec.
import com.deviceinfo.NativeDeviceInfoSpec
class DeviceInfoModule(reactContext: ReactApplicationContext) :
NativeDeviceInfoSpec(reactContext) {
override fun getName() = NAME
override fun getDeviceName(): String = android.os.Build.MODEL
override fun getBatteryLevel(): Double {
val bm = reactApplicationContext
.getSystemService(android.content.Context.BATTERY_SERVICE)
as android.os.BatteryManager
return bm.getIntProperty(
android.os.BatteryManager.BATTERY_PROPERTY_CAPACITY
).toDouble()
}
override fun isBiometricAvailable(promise: com.facebook.react.bridge.Promise) {
// async work still uses a Promise
promise.resolve(true)
}
companion object {
const val NAME = "NativeDeviceInfo"
}
}Consuming it from JavaScript is unremarkable — which is the point. The synchronous methods just return values:
import DeviceInfo from './specs/NativeDeviceInfo';
export function DeviceBanner() {
// These are synchronous JSI calls — no await, no bridge round-trip.
const name = DeviceInfo.getDeviceName();
const battery = DeviceInfo.getBatteryLevel();
return (
<Text>
{name} — {battery}% battery
</Text>
);
}Synchronous is powerful, not free. A synchronous TurboModule method runs on the JS thread and blocks it until it returns. Keep synchronous methods trivial; anything touching disk, network, or heavy computation should stay asynchronous.
What Does Codegen Actually Generate?
You may have noticed that both the TurboModule spec and the Fabric component props are declared in TypeScript, yet native code implements them. Codegen is the tool that bridges that gap. At build time it parses your typed specs (files following the Native* and *NativeComponent conventions) and generates the C++, Objective-C, and Java/Kotlin interface code that guarantees both sides agree on the contract.
What Codegen buys you:
- Type safety across the language boundary: a mismatch between your TS spec and your native implementation fails at compile time, not at runtime.
- Less boilerplate: the marshalling glue between JS values and native types is generated, not hand-written.
- A single source of truth: the TypeScript spec is the contract; native implementations must conform to the generated base class or interface.
Library authors declare their Codegen configuration in package.json so consumers generate the right artifacts automatically during the native build:
{
"name": "react-native-device-info-turbo",
"codegenConfig": {
"name": "RNDeviceInfoSpec",
"type": "all",
"jsSrcsDir": "src/specs",
"android": {
"javaPackageName": "com.deviceinfo"
}
}
}The type field can be modules (TurboModules only), components (Fabric views only), or all. During a build, Codegen output usually lands in build/generated for Android and in the Pods for iOS — useful to know when you are debugging a generated-symbol error.
How Do You Enable the New Architecture?
Since React Native 0.76 the New Architecture is on by default, and the 2025–2026 releases (0.77 through the 0.8x line) have continued hardening it while winding down the legacy bridge. Practically, if you are on a recent version you are probably already running it. Still, you should know the exact switches.
Expo projects
For Expo SDK 51 and later, the New Architecture is controlled through app config and enabled by default from SDK 52 onward. You set it explicitly like this:
{
"expo": {
"name": "my-app",
"newArchEnabled": true,
"ios": { "newArchEnabled": true },
"android": { "newArchEnabled": true }
}
}After changing this, you must regenerate native projects and rebuild — a hot reload will not pick it up. With a development build workflow that means running the prebuild and building fresh binaries:
# Regenerate native directories with the new setting applied
npx expo prebuild --clean
# Build and run a fresh dev client (New Arch compiled in)
npx expo run:ios
npx expo run:androidBare React Native projects
In a bare project the flags live in the native build files. On Android, set the Gradle property; on iOS it is driven by an environment flag during pod install.
# android/gradle.properties
newArchEnabled=true
# iOS — reinstall pods with the New Architecture flag
cd ios && RCT_NEW_ARCH_ENABLED=1 bundle exec pod installHow Do You Verify It Is Actually On?
Do not assume — confirm. The cleanest runtime check is the global bridgeless flag, which is only true when the New Architecture (bridgeless mode) is active:
import { Platform } from 'react-native';
// true only under the New Architecture (bridgeless runtime)
const isNewArch = (global as any)?.RN$Bridgeless === true;
// A Fabric-specific signal: the concurrent root flag is set by Fabric.
const isFabric = (global as any)?.nativeFabricUIManager != null;
console.log('New Architecture:', isNewArch);
console.log('Fabric renderer:', isFabric, 'on', Platform.OS);On startup, a New Architecture app also logs a line like "Running “AppName” with the New Architecture." In development you can additionally inspect the LogBox / dev menu, and on iOS the presence of an RCTFabricSurface in the view hierarchy confirms Fabric is mounting your screens.
Migration Gotchas and Library Compatibility
The New Architecture is stable, but flipping the flag on a mature app is rarely a no-op. These are the issues that actually consume days.
The things that bite in practice:
- Third-party libraries: any library that ships its own native code must be New-Architecture-ready. Most popular libraries (React Navigation, Reanimated 3+, Gesture Handler, Screens, Safe Area Context) are, but check the reactnative.directory site’s "New Architecture" badge before you commit.
- The interop layer is a crutch, not a fix: React Native ships a legacy interop layer that lets old bridge-based modules run under the New Architecture. It works, but it reintroduces async overhead and can misbehave with view managers. Treat it as a migration bridge, not a destination.
- Custom native modules must be rewritten as TurboModules: hand-written NativeModules with RCT_EXPORT_METHOD or ReactMethod need to be converted to typed specs plus Codegen. There is no automatic conversion.
- Direct manipulation APIs are gone: setNativeProps and other imperative escape hatches behave differently or are unsupported under Fabric. Move to state-driven updates or the Reanimated worklet model.
- Layout timing differences: because Fabric commits layout differently, code that relied on onLayout firing at a specific moment, or on synchronous measure quirks, can shift by a frame. Audit any measure-then-position logic.
- findNodeHandle and legacy refs: patterns depending on the old UIManager and node handles may need updating to the new ref and commands APIs.
The single most reliable migration strategy: upgrade React Native and every native dependency to New-Architecture-ready versions first, get a clean build, and only then start converting your own native code. Mixing the two in one pass makes failures impossible to bisect.
What Are the Performance Implications?
The New Architecture is not a magic performance button that makes every app faster overnight. What it does is remove structural ceilings and open new optimization paths. The wins concentrate in specific scenarios.
Where you should expect measurable improvement:
- Startup time: lazy TurboModule initialization means fewer native modules load eagerly, improving time-to-interactive on module-heavy apps.
- Gesture-driven and animation-heavy UI: synchronous JSI access removes the frame of lag that plagued gesture-tracking views, and Reanimated worklets run without bridge round-trips.
- High-frequency native interactions: anything that used to spam the bridge (scroll events, frequent measurements, real-time data) benefits from zero serialization.
- Concurrent rendering scenarios: Suspense, transitions, and streaming UI become viable because Fabric can prepare and commit trees atomically.
The trade-offs are real too. C++ compilation makes native builds slower and more complex, debugging now sometimes means reading generated C++, and synchronous methods give you a new way to block the JS thread if you misuse them. As always, measure with a profiler (Flipper’s successor tooling, Hermes sampling profiler, or Xcode Instruments) rather than trusting intuition.
Key Takeaways
If you remember only a handful of things from this post:
- The bridge is gone. JSI replaces the async, JSON-serialized queue with a direct, synchronous, memory-sharing C++ layer between JavaScript and native.
- Fabric is the JSI-based renderer: immutable shadow trees, C++ Yoga layout, synchronous measurement, and compatibility with React 18 concurrent features.
- TurboModules are lazy (loaded on first access) and can be synchronous — but a synchronous method blocks the JS thread, so keep it trivial.
- Codegen turns your TypeScript specs into type-safe native interfaces, catching JS/native contract mismatches at build time.
- It is the default from React Native 0.76 onward; enable it explicitly with newArchEnabled in Expo app config or the Gradle/pod flags in bare projects, and always rebuild fresh binaries.
- Verify with the global RN$Bridgeless and nativeFabricUIManager flags rather than assuming.
- Migration risk lives in third-party native libraries, custom NativeModules that must become TurboModules, and layout-timing assumptions — upgrade dependencies first, then convert your own code.
The New Architecture has turned React Native from a framework fighting its own async boundary into one that shares a genuine runtime with native. Understanding JSI, Fabric, TurboModules, and Codegen is no longer specialist trivia — it is the mental model you need to reason about performance, debug native issues, and ship libraries that others can actually depend on.
