React Native New Architecture in Practice: JSI, Fabric, and TurboModules Explained
React Native's architecture was fundamentally unchanged from 2015 to 2022. The Bridge - an asynchronous JSON-serialized message bus between JavaScript and native code - worked well enough for most apps, but had hard limits that couldn't be engineered around.
In 2022, Meta started shipping the New Architecture. In React Native 0.74 (2024), it became opt-in stable. In 0.76, it became the default for new projects.
Here's what actually changed, why it matters, and what you need to know to use it effectively.
What was wrong with the old Bridge
The old architecture had a single, asynchronous Bridge between JavaScript and the native layer. Every interaction - calling a native module, updating UI, responding to events - went through this Bridge as serialized JSON strings.
JavaScript (V8/Hermes) ←→ Bridge (JSON serialization) ←→ Native (Swift/Kotlin)
Problems this created:
Asynchronous by default. Calling a native module and getting a result required at least two round trips through the bridge. For time-sensitive operations (touch response, gesture handling), this 1-3ms overhead was noticeable.
JSON serialization cost. Every value crossing the bridge - numbers, strings, objects, arrays - was serialized to JSON on one side and deserialized on the other. For high-frequency data (camera frames, gyroscope readings, scroll position), this was significant CPU overhead.
Single-threaded UI updates. All layout calculations happened on a dedicated native thread, but layout could not be triggered synchronously from JavaScript. Animated gestures would lag because JavaScript couldn't respond synchronously to layout changes.
Startup cost. All native modules had to be initialized at startup, whether used or not, because the bridge needed a complete module registry.
JSI: JavaScript Interface
JSI (JavaScript Interface) replaces the Bridge with a direct C++ binding to the JavaScript engine.
JavaScript (Hermes) ←→ JSI (C++ host objects) ←→ Native (Swift/Kotlin)
The critical change: JavaScript can now hold references to native objects and call native functions synchronously. No serialization. No round trip. Direct function call.
// Old Bridge: JS calls native module
// JS side: NativeModules.Camera.capture(options, callback)
// Serializes to JSON, posts to native thread, native executes,
// posts result back to JS thread via callback
// JSI: JS holds a reference to a C++ object
// JS side: global.__camera.capture(options) - synchronous, direct call
// No serialization, no thread hop for the call itself
In practice, this means:
- Native modules can expose values that JavaScript can read without any async overhead
- JavaScript can call native functions and get synchronous return values when needed
- Typed data (TypedArrays like Float32Array) can be shared between JS and native without copying - critical for camera, audio, ML
TurboModules
TurboModules use JSI to make native modules:
- Lazily initialized - only loaded when first accessed, not at startup
- Strongly typed - type spec is defined in TypeScript, C++ code generated from it
- Synchronous-capable - can return values synchronously via JSI when needed
// TurboModule spec (TypeScript → generates C++ bridge code)
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';
export interface Spec extends TurboModule {
// Synchronous - returns value directly
getDeviceId(): string;
// Async - still Promise for genuinely async operations
getLocationPermission(): Promise<'granted' | 'denied' | 'never_ask_again'>;
// Callback-based
startAccelerometer(interval: number, callback: (data: AccelerometerData) => void): void;
stopAccelerometer(): void;
}
export default TurboModuleRegistry.getEnforcing<Spec>('MyModule');
The startup benefit is real. A large React Native app with 30-40 native modules in the old architecture would initialize all of them at launch. With TurboModules, modules are initialized on first use. In our measurements: 400-700ms startup time reduction for apps with heavy native module usage.
Fabric: the new renderer
Fabric is the new UI rendering engine. It replaces the old UIManager with a C++ core that:
- Runs layout synchronously on both the JS thread and native thread
- Supports concurrent features - React 18's Suspense, transitions, and concurrent rendering
- Enables synchronous native events - touch events can now be handled without async roundtrips
The old renderer calculated layout on a separate "shadow thread" then applied to native views. The new renderer keeps a shadow tree in C++ that mirrors the React tree, enabling direct synchronous communication.
// Old architecture: this animation could stutter
// Because Animated had to cross the bridge on each frame
const opacity = new Animated.Value(1);
// The new Animated (Reanimated 3 with New Architecture)
// runs entirely on the UI thread - zero bridge overhead
import Animated, { useSharedValue, withSpring } from 'react-native-reanimated';
function MyComponent() {
const scale = useSharedValue(1);
// This worklet runs on UI thread, not JS thread
// No bridge, no lag, 60/120fps guaranteed
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
return (
<Animated.View style={animatedStyle}>
<Text>60fps, always</Text>
</Animated.View>
);
}
Real performance differences
From a production app migration (e-commerce, ~120k MAU):
| Metric | Old Architecture | New Architecture | Change |
|---|---|---|---|
| Cold start (iOS) | 2,340ms | 1,680ms | -28% |
| Cold start (Android) | 3,100ms | 2,200ms | -29% |
| Scroll FPS (product list) | 52fps avg | 59fps avg | +13% |
| Gesture response latency | 18ms | 4ms | -78% |
| Memory at startup | 187MB | 164MB | -12% |
| JS bundle parse time | 340ms | 210ms | -38% |
The gesture response latency improvement is the most user-visible. Tapping a button and seeing immediate visual feedback is qualitatively different even at these small absolute numbers.
Migration checklist
If you're migrating an existing app:
# 1. Update to RN 0.74+ (New Architecture opt-in) or 0.76+ (default)
npx react-native upgrade
# 2. Enable New Architecture (if on 0.74/0.75)
# android/gradle.properties
newArchEnabled=true
# iOS: Podfile
ENV['RCT_NEW_ARCH_ENABLED'] = '1'
Native module compatibility: Any native module using the old Bridge API will break. Check whether your dependencies have TurboModule support:
# Check for New Architecture support in your dependencies
npx react-native-community/directory search --filters="new-architecture"
Major libraries with New Architecture support as of 2025:
react-native-reanimated3.x ✅react-native-gesture-handler2.x ✅react-native-screens3.x ✅react-native-camera- usereact-native-vision-camera4.x instead ✅react-native-maps1.10+ ✅react-native-firebase21+ ✅
The interop layer: If you have old-architecture native modules that don't have TurboModule versions, the interop layer lets them still work. Enable it in android/app/src/main/jni/MainApplicationModuleProvider.cpp. This means you can migrate incrementally.
When New Architecture matters most
High-gain scenarios:
- Apps with heavy gesture interactions (drag, swipe, pinch-zoom)
- Camera and media processing
- Real-time data visualization (charts, maps)
- Large lists with complex item layouts
- Apps with many native modules (telemetry, analytics, payments, maps all add up)
Lower impact:
- Simple form-based apps
- Apps with mostly static content
- Apps where network latency dominates user experience
If your app's main bottleneck is API response time, New Architecture won't move your key metrics. Profile first. The 28% startup improvement is real and universal, but the rest depends heavily on what your app does.
The JSI-native module pattern
Writing a TurboModule from scratch for a custom native feature:
// specs/NativeImageProcessor.ts
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';
export interface Spec extends TurboModule {
processImage(
imagePath: string,
operations: ReadonlyArray<{
type: 'resize' | 'compress' | 'crop';
params: Object;
}>
): Promise<string>; // Returns path to processed image
// Synchronous - safe for simple operations
getImageDimensions(imagePath: string): { width: number; height: number };
}
export default TurboModuleRegistry.getEnforcing<Spec>('ImageProcessor');
// iOS: ImageProcessorModule.swift
@objc(ImageProcessor)
class ImageProcessorModule: NSObject, NativeImageProcessorSpec {
// Synchronous - JSI calls this directly on calling thread
func getImageDimensions(_ imagePath: String) -> [String: NSNumber] {
guard let image = UIImage(contentsOfFile: imagePath) else {
return ["width": 0, "height": 0]
}
return [
"width": NSNumber(value: Float(image.size.width)),
"height": NSNumber(value: Float(image.size.height)),
]
}
// Async
func processImage(_ imagePath: String, operations: [[String: Any]], resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
DispatchQueue.global(qos: .userInitiated).async {
// Process image...
resolve(outputPath)
}
}
}
The New Architecture is no longer a preview or a beta. If you're starting a new React Native project in 2025 or 2026, you're using it by default. If you have a production app on the old architecture, the migration is worth doing - the startup improvement alone justifies the work for most apps.
Aunimeda develops mobile applications for iOS and Android - from MVP to production-ready apps with full backend integration.
Contact us to discuss your mobile project. See also: Mobile App Development, Mobile Game Development