MinhVo

Minh Vo

rss feed

Slaying code & making it lit fr fr 🔥 tagline

Hey there 👋 I'm an AI Engineer with 7 years of experience building scalable web and mobile applications. Currently at Neurond AI (May 2025 — present), architecting an Enterprise AI Assistant Platform with multi-tenant RAG on pgvector, multi-provider LLM orchestration, and Azure-native infrastructure. Previously spent 5+ years at SNAPTEC (Sep 2019 — Apr 2025), leading SaaS themes, admin dashboards, and e-commerce platforms — earned the Hero of the Year award in 2021. I specialize in TypeScript, React, Next.js, and AI-Native engineering with Claude Code and Cursor.bio

Back to blogs

React Native New Architecture: TurboModules and Fabric

Explore React Native's new architecture: TurboModules, Fabric renderer, and JSI.

React NativeTurboModulesFabricMobile

By MinhVo

Introduction

React Native's new architecture represents a ground-up reimagining of how JavaScript communicates with native platform code. For years, React Native relied on an asynchronous JSON bridge to connect the JavaScript runtime with native iOS and Android components — a design that worked but introduced inherent latency in event handling, view rendering, and native module invocations. The new architecture eliminates this bridge entirely, replacing it with the JavaScript Interface (JSI), a synchronous C++ layer that enables direct function calls between JavaScript and native code.

This transition affects every layer of the React Native stack. TurboModules replace the old native module system with lazy-loaded, type-safe modules that communicate through JSI. The Fabric renderer replaces the old UIManager with a concurrent, multi-threaded rendering pipeline. And the Codegen system generates type-safe C++ bindings from TypeScript/Flow specifications, eliminating entire categories of runtime errors. Understanding these components is essential for any serious React Native developer.

React Native Architecture

Understanding the New Architecture: Core Concepts

Why the Bridge Had to Go

The original React Native architecture used a serialized bridge to communicate between JavaScript and native code. When your React component called a native method like AsyncStorage.getItem(), the call was serialized to JSON, queued in the bridge, dispatched to the native thread, deserialized, executed, and the result was serialized back through the same pipeline.

This introduced several problems:

Latency: Every native call crossed the bridge asynchronously, adding at least one frame of delay. For gestures and animations, this latency was perceptible.

Serialization overhead: Complex data structures (like large JSON objects or binary data) required expensive serialization and deserialization on every crossing.

Batching limitations: The bridge batched messages for efficiency, but this made the timing of native operations unpredictable, causing race conditions and frame drops.

Type unsafety: Bridge messages were untyped JSON blobs, so type mismatches between JavaScript and native code were only caught at runtime, often manifesting as cryptic crashes.

The JSI Foundation

JSI (JavaScript Interface) is a lightweight C++ abstraction that allows JavaScript to hold direct references to C++ objects (called "host objects") and invoke C++ functions without serialization. Think of it as the equivalent of Node.js's N-API, but designed for React Native's mobile environment.

With JSI, a JavaScript call to a native method becomes a direct C++ function invocation — no JSON serialization, no queueing, no thread-hopping overhead. The result is returned synchronously (or can return a Promise for genuinely asynchronous operations like network requests).

// A JSI host function that JavaScript can call directly
jsi::Value multiply(jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t) {
  double a = args[0].getNumber();
  double b = args[1].getNumber();
  return jsi::Value(a * b);
  // Called directly from JavaScript — no bridge, no serialization
}

Architecture Overview

The new architecture consists of four interconnected layers:

  1. JSI — C++ foundation enabling synchronous JS ↔ native communication
  2. TurboModules — Lazy-loaded native modules with codegen-generated type safety
  3. Fabric — Concurrent rendering system with synchronous view operations
  4. Codegen — Static type generation from TypeScript/Flow to C++/native types

Architecture Layers

Architecture and Component Design

TurboModules Deep Dive

TurboModules are the successor to the legacy NativeModules system. They provide three key improvements:

Lazy Loading: Legacy native modules were all loaded eagerly at app startup, even if never used. TurboModules load on first access, reducing startup time proportionally to the number of native modules in your project.

Type Safety: TurboModule specifications are written in TypeScript or Flow and compiled to C++ type definitions. A mismatch between JavaScript and native types is caught at build time, not at runtime.

Synchronous Access: Through JSI, TurboModule methods can return results synchronously when appropriate, eliminating the async bridge overhead for operations that complete instantly.

Defining a TurboModule Specification

// specs/NativeCalculator.ts
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';
 
export interface Spec extends TurboModule {
  // Synchronous method — returns result directly via JSI
  add(a: number, b: number): number;
 
  // Asynchronous method — returns a Promise
  fetchResult(query: string): Promise<string>;
 
  // Constants exported from native
  getConstants(): {
    PI: number;
    E: number;
    VERSION: string;
  };
 
  // Event emitter support
  addListener(eventName: string): void;
  removeListeners(count: number): void;
}
 
export default TurboModuleRegistry.getEnforcing<Spec>('Calculator');

Native iOS Implementation (Swift)

// ios/Calculator/CalculatorModule.swift
import Foundation
import React
 
@objc(CalculatorModule)
class CalculatorModule: NSObject {
 
  @objc static func requiresMainQueueSetup() -> Bool {
    return false
  }
 
  @objc func add(_ a: Double, b: Double) -> Double {
    return a + b
  }
 
  @objc func fetchResult(_ query: String,
                         resolve: @escaping RCTPromiseResolveBlock,
                         reject: @escaping RCTPromiseRejectBlock) {
    // Simulate async operation
    DispatchQueue.global().async {
      let result = "Result for: \(query)"
      resolve(result)
    }
  }
 
  @objc func constantsToExport() -> [String: Any] {
    return [
      "PI": Double.pi,
      "E": M_E,
      "VERSION": "1.0.0"
    ]
  }
}

Native Android Implementation (Kotlin)

// android/src/main/java/com/app/calculator/CalculatorModule.kt
package com.app.calculator
 
import com.facebook.react.bridge.*
 
class CalculatorModule(reactContext: ReactApplicationContext) :
    ReactContextBaseJavaModule(reactContext) {
 
    override fun getName() = "Calculator"
 
    @ReactMethod(isBlockingSynchronousMethod = true)
    fun add(a: Double, b: Double): Double {
        return a + b
    }
 
    @ReactMethod
    fun fetchResult(query: String, promise: Promise) {
        // Async operation
        Thread {
            val result = "Result for: $query"
            promise.resolve(result)
        }.start()
    }
 
    override fun getConstants(): Map<String, Any> {
        return mapOf(
            "PI" to Math.PI,
            "E" to Math.E,
            "VERSION" to "1.0.0"
        )
    }
}

Using the TurboModule in JavaScript

import Calculator from './specs/NativeCalculator';
 
// Synchronous call via JSI — no bridge, no serialization
const sum = Calculator.add(3.14, 2.86); // Returns 6.0 immediately
 
// Constants accessed without async overhead
const { PI, VERSION } = Calculator.getConstants();
 
// Async operations return Promises
const result = await Calculator.fetchResult('complex-query');

Step-by-Step Implementation

Enabling the New Architecture

React Native 0.71+ (Android):

# android/gradle.properties
newArchEnabled=true

React Native 0.71+ (iOS):

cd ios
RCT_NEW_ARCH_ENABLED=1 pod install

With Expo SDK 49+:

{
  "expo": {
    "plugins": [
      [
        "expo-build-properties",
        {
          "ios": { "newArchEnabled": true },
          "android": { "newArchEnabled": true }
        }
      ]
    ]
  }
}

Running the Codegen

The Codegen generates C++ type definitions from your TurboModule specs:

# Generate native types from specs
npx react-native codegen
 
# The output includes:
# - C++ header files with type-safe interfaces
# - iOS Objective-C++ glue code
# - Android JNI bindings

Migrating a Legacy Module to TurboModules

Step 1: Create the TurboModule spec:

// specs/NativeStorage.ts
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';
 
export interface Spec extends TurboModule {
  getItem(key: string): Promise<string | null>;
  setItem(key: string, value: string): Promise<void>;
  removeItem(key: string): Promise<void>;
}
 
export default TurboModuleRegistry.getEnforcing<Spec>('Storage');

Step 2: Implement the native side (reuse existing code):

// In your JavaScript, replace:
// import { NativeModules } from 'react-native';
// const { Storage } = NativeModules;
 
// With:
import Storage from './specs/NativeStorage';

Step 3: The Codegen generates C++ bindings automatically during the build.

Migration Workflow

Real-World Use Cases

Use Case 1: High-Frequency Data Streaming

For sensor data or real-time analytics, synchronous TurboModule calls eliminate buffering:

// specs/NativeSensor.ts
export interface Spec extends TurboModule {
  // Synchronous — returns current reading instantly
  getCurrentAcceleration(): { x: number; y: number; z: number };
  getCurrentGyroscope(): { alpha: number; beta: number; gamma: number };
  startListening(intervalMs: number): void;
  addListener(eventName: string): void;
  removeListeners(count: number): void;
}
 
// Usage — 60fps sensor polling without bridge bottleneck
import Sensor from './specs/NativeSensor';
 
function useSensorData() {
  const [data, setData] = useState({ x: 0, y: 0, z: 0 });
 
  useEffect(() => {
    const interval = setInterval(() => {
      // Synchronous JSI call — takes ~0.1ms instead of ~2ms via bridge
      const reading = Sensor.getCurrentAcceleration();
      setData(reading);
    }, 16); // 60fps
 
    return () => clearInterval(interval);
  }, []);
 
  return data;
}

Use Case 2: Large Data Transfer Without Serialization

Passing large buffers (images, audio, binary data) through JSI without JSON serialization:

// specs/NativeImageProcessor.ts
export interface Spec extends TurboModule {
  // JSI can pass ArrayBuffers directly — no base64 encoding needed
  processImage(imageData: ArrayBuffer, width: number, height: number): ArrayBuffer;
  getThumbnail(imageData: ArrayBuffer, maxSize: number): ArrayBuffer;
}
 
// Usage — process a 4K image without 30% base64 overhead
import ImageProcessor from './specs/NativeImageProcessor';
 
async function createThumbnail(imageUri: string) {
  const response = await fetch(imageUri);
  const buffer = await response.arrayBuffer();
 
  // Direct ArrayBuffer transfer via JSI
  const thumbnail = ImageProcessor.getThumbnail(buffer, 200);
  return new Blob([thumbnail], { type: 'image/jpeg' });
}

Use Case 3: Shared C++ Code Between Platforms

Use JSI to write platform logic once in C++:

// shared/AnalyticsEngine.cpp
class AnalyticsEngine : public jsi::HostObject {
public:
  jsi::Value get(jsi::Runtime& rt, const jsi::PropNameID& name) override {
    auto methodName = name.utf8(rt);
 
    if (methodName == "trackEvent") {
      return jsi::Function::createFromHostFunction(
        rt, name, 2,
        [](jsi::Runtime& rt, const jsi::Value&,
           const jsi::Value* args, size_t count) -> jsi::Value {
          std::string eventName = args[0].getString(rt).utf8(rt);
          // Process on C++ layer — shared between iOS and Android
          analytics::track(eventName, parseProperties(rt, args[1]));
          return jsi::Value::undefined();
        }
      );
    }
    return jsi::Value::undefined();
  }
};

Best Practices for Production

  1. Migrate modules incrementally: Don't attempt to migrate all native modules at once. Start with the most performance-critical ones (those called frequently or passing large data).

  2. Use Codegen from day one: Write TurboModule specs even for modules that haven't been migrated yet. The Codegen catches type mismatches at build time.

  3. Prefer synchronous methods for simple operations: If a native method completes in under 1ms, make it synchronous. The bridge's async overhead (2-5ms) dwarfs the actual computation.

  4. Test with both architectures: Use feature flags to test your app with old and new architecture side by side during migration.

  5. Profile startup time improvement: Use Systrace or platform-specific profiling tools to measure the startup time improvement from lazy-loaded TurboModules.

  6. Avoid the bridge during migration: During the migration period, avoid patterns where old native modules and new TurboModules communicate through JavaScript — each crossing adds latency.

  7. Update community libraries: Check that your third-party native modules support the new architecture. Most popular libraries have been updated, but niche ones may not be.

  8. Use Shared C++ logic for complex business rules: If you have complex algorithms (encryption, image processing, ML inference), implement them in C++ and expose via JSI for maximum performance on both platforms.

Common Pitfalls and Solutions

PitfallImpactSolution
Third-party native module not updated for new architectureBuild failure or runtime crashFork the module and add TurboModule support, or find an alternative
Codegen output not committed to version controlBuild fails on CI/CDAdd Codegen output to .gitignore exceptions or regenerate in CI
Mixing bridge modules and TurboModulesDouble initialization overheadMigrate related modules together to avoid mixed bridge/JSI communication
Synchronous method blocking the JS threadUI freezesUse async methods for operations that may take >1ms
Memory leaks from JSI host objectsApp memory grows over timeEnsure C++ host objects implement proper destructors and weak references
Type mismatch between spec and implementationSilent data corruptionRun Codegen in CI and fail builds on type mismatches

Performance Optimization

Startup Time with TurboModules

Lazy loading TurboModules eliminates eager initialization overhead:

Old Architecture (all modules eager):
- 50 native modules × ~5ms each = 250ms initialization
- Total bridge setup: ~100ms
- Total startup overhead: ~350ms

New Architecture (lazy TurboModules):
- Only required modules loaded: ~3 modules × 2ms = 6ms
- JSI setup: ~20ms
- Total startup overhead: ~26ms
- Improvement: ~93% reduction in native module startup cost

Call Latency Comparison

Operation: NativeModules.Calculator.add(1, 2)
Old bridge: 2.5ms (serialize → queue → dispatch → deserialize → execute → return)
New JSI:    0.05ms (direct C++ function call)
Improvement: 50x faster

Comparison with Alternatives

FeatureTurboModules/FabricOld ArchitectureFlutter Platform ChannelsKotlin Multiplatform
CommunicationSynchronous (JSI)Asynchronous (Bridge)Asynchronous (Codec)Direct (same process)
Type SafetyCodegen at build timeRuntime checkingProtobuf/manualKotlin types
Lazy LoadingBuilt-inManualN/AN/A
Concurrent RenderingFabricNot supportedImpellerN/A
Cross-Platform Native CodeShared C++ via JSINot possibleShared DartShared Kotlin
Migration EffortIncrementalN/AN/AFull rewrite

Testing Strategies

// Mock TurboModules in tests
jest.mock('./specs/NativeCalculator', () => ({
  __esModule: true,
  default: {
    add: jest.fn((a: number, b: number) => a + b),
    fetchResult: jest.fn((query: string) => Promise.resolve(`Result: ${query}`)),
    getConstants: jest.fn(() => ({
      PI: 3.14159,
      E: 2.71828,
      VERSION: '1.0.0',
    })),
  },
}));
 
import Calculator from './specs/NativeCalculator';
 
describe('Calculator TurboModule', () => {
  it('should add numbers synchronously', () => {
    const result = Calculator.add(2, 3);
    expect(result).toBe(5);
  });
 
  it('should fetch results asynchronously', async () => {
    const result = await Calculator.fetchResult('test');
    expect(result).toBe('Result: test');
  });
});

Future Outlook

The new architecture is the foundation for React Native's next decade. With JSI as the communication layer, future features like React Server Components, streaming SSR for React Native Web, and true concurrent rendering become possible. The Static Hermes AOT compiler will leverage JSI for even faster module calls by inlining native operations at compile time. The bridge deprecation timeline indicates full removal in 2025, making migration urgent for teams maintaining legacy native modules.

New Architecture Performance Gains

The New Architecture delivers performance improvements through several mechanisms. TurboModules use lazy loading, so native modules are only initialized when first called instead of at app startup. Fabric's synchronous layout reduces the bridge traffic between JavaScript and native threads. The JSI eliminates serialization overhead by allowing JavaScript to call native functions directly through C++ bindings. Measure the impact of these changes in your app using React Native's built-in performance monitoring and native profiling tools like Xcode Instruments and Android Studio Profiler.

Interop Layer Compatibility

The React Native New Architecture includes an interop layer that allows legacy native modules to work alongside TurboModules and Fabric components. Enable the interop layer in your React Native configuration to use existing native modules without rewriting them. The interop layer adds a small performance overhead compared to native TurboModules, so plan to migrate high-frequency native modules first. Use the community-maintained compatibility library to identify which popular React Native libraries have been updated for the New Architecture.

Migration Timeline and Prioritization

Plan your New Architecture migration in phases to minimize risk and maintain release velocity. Phase one enables the interop layer and verifies that all existing native modules work correctly. Phase two migrates high-impact modules to TurboModules, starting with modules called frequently from JavaScript (analytics, device info, networking). Phase three converts UI components to Fabric, prioritizing components that use complex layout or animation. Each phase should include performance benchmarking to validate the expected improvements.

The bridge deprecation timeline requires all React Native applications to migrate to the New Architecture by 2025. Libraries that haven't been updated will need either community forks or replacement alternatives. Audit your dependencies early and create a migration plan for each library. The React Native community maintains a compatibility spreadsheet tracking the New Architecture status of popular libraries, which helps identify potential blockers before you begin migration.

Conclusion

React Native's new architecture — TurboModules, Fabric, and JSI — represents the most significant technical overhaul in the framework's history. By replacing the asynchronous bridge with synchronous C++ communication, it eliminates the performance gap that has been React Native's primary limitation.

Key takeaways:

  1. JSI eliminates the bridge — direct C++ function calls replace JSON serialization
  2. TurboModules load lazily — only modules you use are initialized, improving startup time
  3. Codegen catches type errors at build time — no more runtime crashes from mismatched native types
  4. Fabric enables concurrent rendering — frame-perfect gestures and animations
  5. Migration is incremental — bridge and JSI modules coexist during transition
  6. Synchronous methods for simple operations — 50x latency improvement over the bridge
  7. Shared C++ code works on both platforms — implement business logic once, run everywhere

Begin your migration today. Start with your most performance-critical native modules, enable the new architecture in a feature branch, and measure the improvement. The future of React Native is synchronous, type-safe, and fast.