Mobile Apps

Adopting React Native New Architecture: Fabric and TurboModules

This article walks through the practical steps to enable Fabric and TurboModules in an existing RN app, including codegen setup, bridging migration, and common pitfalls.

Mohammed Saqib12 min read
A developer writes code on a laptop in front of multiple monitors in an office setting.
Photo by Christina Morillo on Pexels · Pexels License

React Native’s bridge was always a bottleneck. Every JS-to-native call had to be serialized into JSON-like messages, queued, and asynchronously processed on a separate thread. That design made startup slow and animations janky on low-end devices, and it forced native module authors to maintain two parallel code paths. The New Architecture replaces that bridge with a synchronous JavaScript Interface (JSI) and a C++ core, but migrating an existing app is not a flip of a single flag. You need to reconfigure your build, regenerate native module specs, and audit every dependency. This article walks through the practical steps to enable Fabric and TurboModules in an existing RN app, including codegen setup, bridging migration, and common pitfalls.

What the New Architecture actually changes

Fabric replaces the bridge with synchronous rendering via the JavaScript Interface (JSI), moving layout calculation to the UI thread. In the old architecture, the UI thread received a serialized instruction tree from the bridge and had to reconstruct it before committing the frame. Fabric keeps a shadow tree in C++ that is updated synchronously from JS, so the UI thread can commit changes immediately. The result is that layout and paint happen in the same frame, which eliminates the visual flicker you sometimes saw with the old bridge on fast state updates.

TurboModules enable lazy loading and direct native function calls without serialization, reducing startup time. Instead of registering every native module at launch, TurboModules create a C++ host object per module. When JS first accesses that object, the native implementation is instantiated on demand. Calls then go through JSI directly, skipping the JSON round-trip entirely. For a large app with dozens of native modules, this can cut several hundred milliseconds off cold start because modules like Networking or AsyncStorage are not initialized until you actually use them.

The C++ core is shared across platforms, which impacts how native module authors write their code – they now implement generated specs instead of manual bridge methods. In the old world, you wrote an RCT_EXPORT_METHOD macro in Objective-C or a @ReactMethod annotation in Kotlin, and the bridge inferred the argument types at runtime. Now you define a TypeScript or Flow spec, and codegen produces a native interface you must implement. This removes a whole class of type-mismatch bugs but forces every module to be rewritten once.

Prerequisites and compatibility checks

Requires React Native 0.68 or later; 0.70+ is strongly recommended for a stable experience. The New Architecture was marked stable in 0.76, but the earlier releases had breaking changes in codegen and the interop layer. If you are on 0.64 or below, you should upgrade React Native itself before attempting this migration. I would not start this work on anything older than 0.72, because the tooling around codegen was still rough.

Hermes must be enabled on both iOS and Android – Fabric depends on JSI, which Hermes provides. JSC is not supported. The bridge could work with either engine, but Fabric requires a JavaScript engine that exposes a proper JSI binding. Hermes is the only engine that ships with a complete JSI implementation for both platforms. If you have not enabled Hermes yet, do that first, verify your app runs correctly, and then proceed. Enabling Hermes can itself surface subtle issues with setTimeout timing or eval usage in third-party libraries.

Audit every third-party native module for New Architecture support. Modules not yet migrated will need a fallback or a fork. The React Native ecosystem has a directory of compatible libraries that is reasonably current. For each module in your package.json that has native code, check whether it publishes a spec file and whether its native implementation has been updated. You can also run npx react-native new-arch --check to get a report of which dependencies are compatible.

Step-by-step migration: codegen and configuration

Enable the New Architecture in gradle.properties (newArchEnabled=true) and set :new_arch_enabled => true in the Podfile. Here is the Android side:

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

And the iOS side in your ios/Podfile:

ENV['RCT_NEW_ARCH_ENABLED'] = '1'
 
platform :ios, '13.0'
 
target 'YourApp' do
  config = use_native_modules!
 
  use_react_native!(
    :path => config[:reactNativePath],
    :new_arch_enabled => true
  )
end

After setting these, run bundle exec pod install on iOS and ./gradlew clean on Android. The build will fail if you have unmigrated native modules that do not implement the generated specs – that is expected.

Run npx react-native new-arch --generate to create type-safe specs for your custom native modules from JavaScript flow types. For each of your own native modules, you need a spec file that defines the interface. Here is an example for a simple module that returns a device token:

// NativeDeviceToken.ts
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';
 
export interface Spec extends TurboModule {
  getDeviceToken(): Promise<string>;
}
 
export default TurboModuleRegistry.getEnforcing<Spec>('NativeDeviceToken');

Codegen will then generate NativeDeviceTokenSpec.h for iOS and NativeDeviceTokenSpec.kt for Android.

Update each native module to implement the generated interfaces (e.g., NativeExampleModuleSpec in Kotlin/Obj-C), and remove old RCT_EXPORT_MODULE macros. On iOS, you replace the RCT_EXPORT_MODULE() macro with a @objc(NativeDeviceToken) annotation and conform to the generated protocol:

// NativeDeviceToken.mm
#import "NativeDeviceTokenSpec.h"
#import <React/RCTBridgeModule.h>
 
@interface NativeDeviceToken () <NativeDeviceTokenSpec>
@end
 
@implementation NativeDeviceToken
 
RCT_EXPORT_MODULE()
 
- (void)getDeviceToken:(RCTPromiseResolveBlock)resolve
                 reject:(RCTPromiseRejectBlock)reject {
  // ... fetch token ...
  resolve(token);
}
 
@end

On Android, you implement the generated Kotlin interface:

// NativeDeviceToken.kt
package com.yourapp.nativemodules
 
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.yourapp.generated.NativeDeviceTokenSpec
 
class NativeDeviceToken(reactContext: ReactApplicationContext) :
    ReactContextBaseJavaModule(reactContext), NativeDeviceTokenSpec {
 
    override fun getName() = "NativeDeviceToken"
 
    override fun getDeviceToken(promise: Promise) {
        // ... fetch token ...
        promise.resolve(token)
    }
}

For modules that are not yet migrated, use the react-native.config.js unstable_newArchEnabled: false flag to retain the old bridge behavior. This is the interop layer – a compatibility shim that lets a legacy module run on the bridge while the rest of the app uses Fabric. It is not free: every call to that module still serializes, and you lose the startup benefit for that module. But it lets you migrate incrementally.

How to handle third-party native modules

Many popular libraries (e.g., react-native-reanimated, react-native-gesture-handler) already support the New Architecture – check their documentation for version requirements. Reanimated has supported Fabric since version 3.0, and gesture-handler since 2.9. But each library has its own quirks. For instance, react-native-screens requires you to enable screensEnabled() in a specific order relative to the new arch flag. Do not assume that a library that "supports" the new architecture works correctly without testing – the interop layer can mask issues that only appear under load.

For libraries that are not yet compatible, you have three options: wait for an update, fork and implement the spec yourself, or keep the old bridge active and accept the performance penalty. Forking is rarely worth it unless the library is small and you control the native code. The interop layer is the pragmatic choice for most apps.

Use the compatibility layer: set newArchEnabled: false per module in react-native.config.js to force the old bridge for that module while the rest of the app runs on the new architecture. Here is an example:

// react-native.config.js
module.exports = {
  dependencies: {
    'react-native-legacy-module': {
      platforms: {
        ios: { unstable_newArchEnabled: false },
        android: { unstable_newArchEnabled: false },
      },
    },
  },
};

This flag makes that module go through the bridge, but the rest of your app still uses JSI. You will not get the full performance benefit, but you can migrate gradually.

Fabric vs old architecture: performance and trade-offs

Fabric eliminates bridge serialization overhead, giving faster rendering and smooth animations, but increases memory usage because of the C++ objects that remain in memory. The shadow tree is not garbage-collected the way the old bridge's serialized instructions were – it is a persistent C++ graph. For a screen with hundreds of views, this can add a few megabytes. On low-memory Android devices, you may see more frequent background kills if you are already close to the memory cap.

TurboModules reduce app startup time (by 10-30% in typical apps) but require every native module to be migrated to see that benefit – a single legacy module can still trigger the old bridge. The interop layer initializes the bridge for that module, which defeats the lazy-loading advantage. If you have one unmigrated module that is imported at the top level of your JS bundle, the bridge spins up anyway. You need to migrate all modules that are imported eagerly to get the full startup win.

Debugging is harder: you lose the old bridge logs, and JSI errors are cryptic. Flipper plugins for the New Architecture are still maturing. The old bridge gave you a clear log line for every native call, with arguments and return values. JSI calls are synchronous and in-process, so there is no equivalent trace. You will often see a EXC_BAD_ACCESS in native code with no stack trace that points to your JS. The React Native New Architecture docs recommend using the RCT_NEW_ARCH_ENABLED build flag to get more verbose logging, but it is still sparse compared to the old bridge.

Do not migrate if your app relies heavily on unimodules (e.g., expo modules in bare workflow) or if the app is stable and performance is not a bottleneck – the migration cost may outweigh the gains. The table below summarizes the key differences:

Aspect Old Architecture New Architecture
Native call mechanism Async, serialized JSON over bridge Sync, direct JSI call
Module initialization All modules at startup Lazy on first use
Layout engine Yoga 1.x (JS thread) Yoga 3.0 (UI thread)
Memory footprint Lower, transient objects Higher, persistent C++ objects
Debugging Rich bridge logs Sparse JSI errors
Migration effort N/A Rewrite native module specs

Common failure modes and debugging strategies

Crash on startup with 'Native module not found' – check that codegen specs are generated and imported in your native modules. Ensure the module's implementation conforms to the generated interface. This is the most common failure. The error message is misleading because the module is registered – but under the new architecture, the registration happens through the generated spec, not the legacy RCT_EXPORT_MODULE macro. If you forgot to run codegen after adding a new module, or if your native implementation does not match the spec's method signatures exactly (e.g., a missing Promise parameter), you get this crash. Verify that the generated files exist in your build directory and that your native class implements the spec interface.

Layout flickers or incorrect sizing – Fabric uses Yoga 3.0 which has different default behavior for minHeight and aspectRatio. Test on both platforms and adjust styles if needed. Yoga 3.0 changed how aspectRatio interacts with flex and how minHeight is resolved when combined with percentage heights. If you have styles that rely on the old behavior, you will see elements that are a few pixels off or that collapse entirely. The fix is usually to add an explicit height or use flexBasis instead of relying on minHeight defaults. I have seen this manifest as a one-frame flicker on iOS when a screen first mounts.

Use the Flipper plugin for the New Architecture to inspect JSI calls and native module registration. Enable RCT_NEW_ARCH_ENABLED=1 logging at build time to see detailed module loading logs. In Flipper, the "React Native" plugin has a section for new arch that shows which modules are loaded and whether they went through the interop layer. That is often the fastest way to confirm that a module you think is migrated is actually still using the bridge.

Rollback and gradual adoption

The New Architecture is an all-or-nothing flag per app – you cannot enable it for only some screens. However, you can revert by simply toggling the flag and cleaning the build. Set newArchEnabled=false in gradle.properties and remove the ENV['RCT_NEW_ARCH_ENABLED'] line from the Podfile, run pod install and ./gradlew clean, and you are back on the old bridge. The codegen-generated files will still be there, but they are ignored when the flag is off.

Create a separate branch for the migration, run your full test suite (especially integration tests for native modules), and monitor crash rates in a beta release before merging to main. The migration touches every native module, so your unit tests for JS logic are less useful here. What matters is that every screen renders correctly and every native call returns the right data. If you have Detox or Maestro tests, run them on both iOS and Android. Pay attention to crash-free sessions in your analytics – a subtle JSI bug can cause a rare crash that only shows up in production.

If issues persist, fall back to the old architecture while you fix individual modules. There is no rush – the old architecture will be supported for at least two more years. The React Native team has committed to maintaining the legacy bridge for the foreseeable future, so you are not on a deadline. I have seen teams spend three months on this migration and then roll back because a critical payment module was not compatible. That is fine. The migration is not a checkbox – it is a performance optimization, and it should be treated like one. If you cannot measure the benefit after migrating, you should not have done it in the first place.

Key takeaways

  • The New Architecture requires Hermes, React Native 0.70+, and codegen-generated specs for every custom native module; the interop layer can handle unmigrated third-party modules but defeats the startup benefit.
  • Fabric gives you synchronous rendering and smoother animations at the cost of higher memory usage; TurboModules cut startup time by 10-30% but only if every eagerly-imported module is migrated.
  • The migration is reversible via a single flag, but debugging JSI crashes is harder than the old bridge – budget extra time for native-level investigation.
  • Do not migrate if your app is stable and performance is acceptable; the legacy bridge will be supported for years, and the migration cost can exceed the gains.
  • For a related performance pitfall in a different stack, see Prompt Caching: What Actually Gets Cached and Why Your Hit Rate Is Zero – it is a good reminder that "new architecture" does not automatically mean "better defaults."

Frequently asked questions

Can I use New Architecture with Expo?
Expo SDK 49+ supports the New Architecture, but it's limited to bare workflow or custom dev clients. Managed workflow does not allow the native module changes required for Fabric and TurboModules, so you'll need to eject or use a development build if you need full control.
Does Fabric require Hermes?
Yes, Fabric depends on Hermes for JSI and synchronous layout. If your app currently uses JavaScriptCore, you must switch to Hermes (set `hermesEnabled = true` in `android/app/build.gradle` and `:hermes_enabled => true` in your Podfile) before enabling the New Architecture.
Will my existing native modules break?
Existing native modules that use the old bridge API will continue to work in a compatibility mode, but they won't benefit from the performance improvements. To fully adopt TurboModules, each module must implement the generated C++ or Kotlin/Obj-C interfaces. Many popular libraries have already been updated, but check the module's documentation before migrating.
#react-native#new-architecture#fabric#turbo-modules#migration
Share