React Native’s New Architecture: What Changed, How to Migrate, and What to Measure

React Native lets us build native mobile interfaces with React, but the way JavaScript communicates with the platform affects what those interfaces can do. The New Architecture changes that foundation: how native functions are called, how views are rendered, and how updates are scheduled.
For developers maintaining an older app, understanding those changes helps answer three practical questions: why migrate, what needs updating, and how can we tell whether the migration improved the app?
When did the New Architecture become available?
There are three milestones to distinguish:
React Native release | What changed |
|---|---|
0.68 — March 2022 | The New Architecture became available as an experimental opt-in. |
0.76 — October 2024 | It became the default and was declared ready for production. |
0.82 — October 2025 | React Native began running exclusively on the New Architecture. Disabling it no longer works. |
These are different stages of adoption: first availability, production readiness, and becoming mandatory. Sources: 0.68 announcement, 0.76 announcement, and 0.82 announcement.
Expo has its own milestones. SDK 52 enabled it for newly created projects; SDKs 53 and 54 enabled it by default. SDK 54 is the last Expo SDK that supports opting out. SDK 55 and later require it. SDK 55 uses React Native 0.83. Expo architecture guide
How the old architecture worked

Figure 1. The legacy architecture’s bridge-based communication model. JavaScriptCore is shown as an example; legacy apps could also use Hermes. Metro belongs to the build pipeline, and “JSON” is shorthand for serialized bridge messages.
In the legacy architecture, often associated with the Paper renderer, JavaScript and native code generally communicated through an asynchronous, batched bridge. Calls and their arguments were converted into serializable messages, queued, and processed on the other side.
For example, JavaScript could request a native operation and receive its result later through a callback or Promise. This separation was useful, but frequent messages, large payloads, and coordination between JavaScript and native UI introduced overhead.
The deeper limitation was timing. Some interactions need immediate layout information or an urgent UI update. An asynchronous communication path made those interactions difficult and could produce visible intermediate states, such as a tooltip briefly appearing in the wrong position. These constraints also prevented full support for React’s concurrent rendering features. React Native’s explanation of the redesign
Calling this a “JSON bridge” is useful shorthand, but avoid picturing every call as an actual JSON string sent through a network-like channel. The architectural issue is serialized, queued communication. Also avoid the absolute claim that legacy native modules could never expose synchronous methods: special blocking APIs existed, even though asynchronous calls were the usual model. Legacy synchronous methods
Understanding the components and their effect on performance

Figure 2. A broad view of the New Architecture. The thread labels are simplified: Metro runs during development and builds, Fabric work can span threads, and TurboModules do not share one universal native thread. Hermes is a separate engine choice. JSI avoids the legacy bridge but does not eliminate all conversion or copying costs.
These names describe different responsibilities. Hermes executes JavaScript, JSI connects the runtime to native code, Fabric manages rendering, and TurboModules expose native functionality. Codegen supplies typed integration code during the build. Understanding these responsibilities helps explain where a performance improvement can come from.

Figure 3. A conceptual overview of a production Hermes build. Fabric coordinates native views; TurboModules expose native features. The arrows show relationships, not thread boundaries.
Hermes: the engine that executes your JavaScript
An engine runs the JavaScript behind your components, event handlers, and business logic. Hermes is Meta’s open-source JavaScript engine optimized for React Native, with a focus on startup, memory usage, and app size.
In a typical production Hermes build, JavaScript is compiled into bytecode during the build. Bytecode is an instruction format the engine can execute; it is not native machine code. Preparing it before installation reduces the JavaScript parsing and compilation work needed when the user opens the app. This can shorten startup. Hermes can also use less memory than JavaScriptCore, although the outcome depends on the application. Using Hermes
Imagine opening an app just to check a delivery status. Reducing the engine’s preparation work can help the first useful screen appear sooner. It does not shorten the server’s response time or remove expensive calculations from your application.
Hermes is separate from the New Architecture: legacy apps could already use it. If both your baseline and migrated builds use the same Hermes version and settings, bytecode compilation is already present in both; it is not a new gain caused by enabling Fabric.
What to measure: cold startup, JavaScript initialization time, and process memory. Keep the engine configuration constant when measuring the architecture migration itself.
JSI: how JavaScript reaches native code
JSI stands for JavaScript Interface. It is a C++ interface to a JavaScript runtime, allowing native integration to expose functions and objects to JavaScript. It is neither an engine nor a thread.
The legacy bridge generally packaged native requests into serialized, queued messages. JSI-based integration can avoid that route. Removing message packaging and bridge scheduling can reduce the overhead of crossing between JavaScript and native code, particularly for frequent calls.
A small native getter illustrates the difference: if the API exposes a synchronous method, JavaScript can receive its value directly instead of scheduling a callback for a later bridge response. Asynchronous methods still belong in this system. Native communication in the New Architecture
The distinction matters because less communication overhead does not mean less work inside the operation. Reading a cached value and processing a large image have very different costs. Making the image operation synchronous could block JavaScript for its entire duration. JSI also does not guarantee zero-copy data transfer; conversions and allocations depend on the API implementation.
What to measure: the latency of representative native calls, the number of calls in a user flow, and time spent converting data versus performing the actual native work.
Fabric: how React updates become native views
Fabric is React Native’s renderer. When a component returns elements such as <View> and <Text>, the renderer coordinates the native views that will represent them on screen.
Its pipeline has three broad phases:
- Render: React executes component logic, and Fabric builds a
C++shadow tree describing native components. - Commit: layout is calculated, using Yoga, and the next tree is prepared.
- Mount: changes are applied to the actual native views.
The shadow tree is an internal representation of the UI, not the visible UI itself. Fabric can share unchanged parts between tree versions, reducing the copying needed for updates. It also determines which host-view changes are necessary. Render, commit, and mount
Fabric helps performance and responsiveness through better update coordination. It supports different priorities and synchronous layout access, enabling modern React rendering features. For example, a tooltip can use layout information to position itself before display, avoiding an intermediate frame in the wrong location. View flattening can also avoid unnecessary native wrapper views. Fabric benefits
Consider a search screen where typing updates both an input and a large results list. With appropriately scheduled React updates, urgent input feedback can take priority over rendering results. This improves responsiveness without implying that the total amount of work is smaller. Concurrent rendering also does not automatically move a large filtering calculation onto another thread.
Fabric is not confined to a dedicated “Fabric thread.” Much of the render pipeline commonly runs on the JavaScript thread, while native view mutations occur on the UI thread; urgent updates can follow other supported paths. Threading model
What to measure: input-to-display latency, missed frames during interactions, and visible layout jumps. A smoother response under load can matter even when average render time barely changes.
TurboModules: native features loaded when needed
A native module exposes platform functionality to JavaScript: storage, device information, or access to a native SDK, for example. TurboModules are React Native’s new system for implementing these interfaces.
They support lazy initialization: a module can be initialized when first requested, instead of contributing that work to startup. If your launch screen does not use a particular native feature, deferring its module initialization can reduce startup work and initial memory use. TurboModules also use the new native communication infrastructure. New native modules
For example, suppose an app only requests a document-scanning module when the user opens its scanning screen. Deferring that module’s initialization can help the launch path. This is an illustrative scenario: the benefit depends on the library’s implementation and whether app code accesses it early.
There is a tradeoff: deferred initialization may appear as additional latency the first time the feature is used. Lazy loading does not automatically remove the module from the installed binary, and modules requested at startup still need to initialize then.
TurboModule APIs can expose synchronous methods or asynchronous results. Their typed specification describes that interface, while native code implements the operation. A camera capture should not be made synchronous simply because synchronous access is available. TurboModule implementation guide
What to measure: startup, initial memory, first use of a native feature, and subsequent uses. Measure both sides of the initialization tradeoff.

Figure 4. Build-time preparation and runtime access are separate. Codegen generates interfaces; developers supply the native implementation. Initialization can occur when the module is first requested, including during import.
A small TurboModule example
This illustrative module returns a cached status label. Its TypeScript specification declares the native method:
// specs/NativeStatus.ts
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';
export interface Spec extends TurboModule {
getLabel(): string;
}
export default TurboModuleRegistry.getEnforcing<Spec>(
'NativeStatus',
);Application code can then call it:
import NativeStatus from './specs/NativeStatus';
const label = NativeStatus.getLabel(); // e.g. "Ready"getEnforcing requests the registered module and throws if it is unavailable. Because it runs at the top level of the specification file, importing that file can trigger the request; lazy initialization does not necessarily wait until getLabel() is called.
The string return type describes a synchronous API. The native implementation should return its cached value quickly, because the caller waits for completion. An asynchronous method would instead declare a Promise result and use an appropriate native implementation.
This is an interface-and-usage example, not a complete runnable module. It also needs Codegen configuration, native implementation, registration, and a new native build. It will not become available in Expo Go by pasting these snippets. The structure follows the React Native TurboModule guide.
In Figure 4, the registry call corresponds to steps 1–2; getLabel() corresponds to steps 3–4. The potential savings come from deferring setup and reducing bridge overhead, not from the TypeScript call itself.
Codegen: the contract between JavaScript and native code
Codegen runs during the build. It reads supported TypeScript or Flow specifications and generates supporting C++ and platform-specific integration code. The native implementation must conform to those generated interfaces. Codegen documentation
This creates a consistent contract for method arguments, return values, and component properties. Its main practical benefits are integration correctness and less handwritten glue code. It supports the new infrastructure, but it should not be presented as an independent promise of faster application logic.
How the pieces work together
Imagine opening a screen that reads a saved preference through a TurboModule and displays the result:
- Hermes executes the JavaScript for the screen.
- The app requests the TurboModule, initializing it if necessary.
- JSI-based bindings connect that request to the native implementation, using interfaces prepared with Codegen.
- The returned value is used to update React state.
- Fabric coordinates the corresponding native UI update.
This is a conceptual example, not a universal pipeline: Fabric and TurboModules serve different purposes, and rendering does not have to pass through a TurboModule.
Component | Potential improvement | Limitation to check |
|---|---|---|
Hermes | Less engine preparation at launch; lower memory in some apps | Existing Hermes apps already have these benefits |
JSI | Less overhead for JavaScript/native interaction | Native work, conversions, and blocking still cost time |
Fabric | Better interaction scheduling and layout coordination | Expensive JavaScript and native drawing can still cause delays |
TurboModules | Deferred initialization reduces launch work | First use can bear the deferred cost |
Codegen | More consistent, typed native integration | Does not optimize your business logic |
These mechanisms explain why improvements are possible. The measurement plan later in this article determines which ones actually benefit your app.
Where does Metro fit?
Metro belongs to the build and development pipeline: it resolves and transforms application code into a bundle. For a production Hermes build, that bundle is then compiled into bytecode. Metro is not a runtime service executing inside the app’s JavaScript thread. TypeScript is available with either architecture. Metro’s build stages
Concern | Legacy architecture | New Architecture |
|---|---|---|
Typical native communication | Batched, serialized bridge messages | Native integration through JSI-based infrastructure |
Renderer | Paper | Fabric |
Native module system | Legacy Native Modules | TurboModules, with typed Codegen specifications |
Update coordination | Constrained by asynchronous architecture | Supports synchronous coordination and concurrent rendering capabilities |
JavaScript engine | Could use Hermes or JavaScriptCore | Engine choice is separate from the architecture concept |
Expected performance | Depends on the app | Depends on the app; improvements must be measured |
How to migrate an Expo app
1. Establish a baseline and choose a migration checkpoint
Create a migration branch and record the current SDK, React Native version, dependencies, and performance of key flows.
For an app still using the legacy architecture, React Native’s migration guidance recommends React Native 0.81 or Expo SDK 54 as a transition point. Enable and validate the New Architecture there before moving beyond the versions that support both. Treat this as a migration checkpoint, not a long-term target. Official migration guidance
Upgrade older Expo projects one SDK at a time and follow each release’s instructions. For example, when your next target is SDK 54:
npm install expo@^54.0.0
npx expo install --fix
npx expo-doctor@latestUse the version range for your actual target SDK. Expo’s upgrade process also requires updating native projects and reviewing release-specific breaking changes. Expo upgrade walkthrough
2. Check native dependencies
Expo Doctor checks library information from React Native Directory. Investigate incompatible, unmaintained, or untested packages, especially those containing native code. A clean report is a useful starting point, not proof that every feature works.
Interop layers let many legacy libraries work, but compatibility is incomplete. Modules built with the Expo Modules API support the New Architecture by default; custom legacy native modules may need additional work. [Expo compatibility guidance](https://docs.expo.dev/guides/new-architecture/)
3. Enable it and rebuild
On the SDK 54 migration checkpoint, merge this setting into your existing app configuration:
{
"expo": {
"newArchEnabled": true
}
}For SDK 55+, it is mandatory; remove any obsolete `newArchEnabled: false` setting. Use a new native build to test the change. Expo Go supports only the New Architecture, so it cannot provide an old-versus-new comparison. Expo configuration guidance
If you use Continuous Native Generation and the native folders are reproducible from app configuration and plugins, regenerate them before building:
npx expo prebuild --clean
npx expo run:android
# Or, on macOS with the iOS toolchain:
npx expo run:ios--clean replaces generated native projects. If you maintain manual changes in ios or android, apply the native upgrade changes explicitly instead of erasing those directories. For EAS builds, ensure the submitted native projects or generated configuration reflect the migration. Native project upgrade instructions
An over-the-air JavaScript update cannot replace the native runtime in an installed app: distribute a new binary for this migration and ensure your EAS Update runtime version distinguishes incompatible runtimes. Expo runtime versions
4. Validate real user flows
Test navigation, gestures, lists, keyboard behavior, modals, accessibility, and native integrations on both platforms. Pay particular attention to custom views, layout measurements, camera operations, maps, and background behavior.
Once the SDK 54 build works with the New Architecture, continue upgrading incrementally to your intended supported SDK. Rebuild and retest at each step.
What benefits should you expect?
The strongest architectural benefits are richer rendering capabilities, improved coordination with native UI, and typed native integration. Fabric supports concurrent features and synchronous layout reads, while Codegen formalizes interfaces across the JavaScript/native boundary. Fabric benefits
Performance gains depend on the bottleneck. An app limited by native-call overhead may benefit differently from one dominated by image decoding, network delays, or expensive JavaScript. Lazy initialization may shift work away from startup, so measure the first use of a feature too.
Avoid universal claims such as “startup falls from three seconds to one” or “memory drops by 40%.” Those require measurements from a named app, device, build, and test procedure. Lazy module loading also does not automatically make the JavaScript bundle smaller.
How to identify and measure the gains
Treat the migration as an experiment. The following is a proposed measurement plan, not a published React Native benchmark.
Compare equivalent builds
Where possible, create two builds on SDK 54 with the same code, dependency versions, Hermes configuration, and build settings. Change only the architecture flag. This helps separate architecture effects from SDK and engine upgrades.
If dependencies prevent that comparison, describe the result as the effect of the complete upgrade. Do not attribute every improvement to Fabric or TurboModules.
Use release builds on physical devices for user-facing timing measurements. Development mode adds overhead that can distort results. Use separate profiling sessions to investigate causes. React Native performance guidance
Measure outcomes users experience
Metric | Define a repeatable scenario |
|---|---|
Cold startup | Process launch to a specified screen being visible and usable |
Interaction latency | Tap or keystroke to visible feedback |
Frame smoothness | Missed frames during a fixed scroll or navigation sequence |
Memory | Process memory after startup, after a heavy flow, and after leaving it |
Native feature latency | First and subsequent uses of a representative native operation |
Reliability | Crashes, Android ANRs, and functional regressions |
Use React profiling to investigate component work and native profilers to investigate CPU activity and platform behavior. React commit duration alone does not measure time until pixels reach the display. React Native documents native profiling with Android Studio and Instruments. Profiling guide
Keep device, OS, data, cache conditions, and network behavior consistent. Alternate old and new runs to reduce ordering bias, and let devices cool between demanding tests. Begin with repeated runs, then increase the sample count if results are noisy. Report sample size, median, and variability; tail percentiles need enough observations to be meaningful.
The New Architecture gives React Native a stronger foundation; the real measure of progress is how your app feels in the hands of its users.