Experiment With the New Architecture of React Native

Hanna Sobolewska
Callstack Engineers
11 min readFeb 27, 2023

by Oskar Kwaśniewski

The following article is part of The Ultimate Guide to React Native Optimization. Including valuable insights from React Native Core Team, it explains the benefits of experimenting with the New Architecture of React Native early on.

Why is this important?

It might be that your app is using an old architecture without the concurrent features of React 18. Or perhaps it’s better to say “current” architecture since it’s still mostly used by production apps. Either way, if you went for New Architecture, you could be leveraging the capabilities of the new rendering system inside your app — and this could greatly benefit your product and improve business results. In this blog post, we’re going to explain what New Architecture is and how it can improve your React Native project.

In other blog posts based on The Ultimate Guide to React Native Optimization, we touch on the following performance-related topics:

Check them out! Now let’s jump into the main topic.

Old vs. New Architecture

Both new and old architecture are based on the communication between JavaScript and the native side. Currently, this communication is handled by the bridge. Let’s go over its limitations to understand better the problems that the New Architecture is trying to solve:

  • It is asynchronous: the JavaScript side submits data to a bridge and waits for the data to be processed by the native side.
  • It’s single-threaded, so it’s important not to overload the JS thread and execute animations on the UI thread.
  • It adds additional overhead when it comes to the serialization of data from JSON objects.

The bridge is still working fine for most use cases. However, when we start to send a lot of data over the bridge, it may become a bottleneck for our app. This issue can be seen when rendering a lot of components in a long list. When the user scrolls fast, there will be a blank space caused by the communication between the JS and native sides being asynchronous. Essentially, we are having a “traffic jam” on our bridge with objects waiting to be serialized. The same issue with the bridge being “overloaded” can be seen in native modules sending a lot of data back and forth.

This bottleneck, together with providing a type-safe way of communicating between native and JS, are the main things that the new architecture is trying to solve. However, not everything about new architecture is as good as it may seem. We will also get into the drawbacks that it brings.

What is New Architecture?

Starting from React Native 0.68, developers can leverage new capabilities of the framework. The New Architecture relies on a series of tools, which are key components to the new experience.

Two most important ones are Fabric and TurboModules. The first one is a new rendering system, and the second one is a new way of writing native modules. We will get into details later in this section.

Codegen and JSI are two new tools improving the developer experience. They are essential to understand how the new architecture works. Codegen drastically improves DX by generating a lot of native boilerplate code and ensuring type safety. And JSI, a C++ API for interacting with any JS engine.

Note: Prior to React Native v0.71+ it’s possible but way harder to deploy the New Architecture in an app. So that’s the version we are suggesting for you to upgrade.

Codegen

A code generation tool that makes JS a source of truth by automating the compatibility between JS and the native side. It allows writing statically typed JS (called JS Spec), which is then used to generate the interface files needed by Fabric native components and TurboModules. Spec consists of a set of types written in TypeScript or Flow that defines all the APIs provided by the native module.

Codegen ensures type-safety and compile-time type safety, which means smaller code and faster execution as both realms can trust each other around validating the data every time. To find out more about it, refer to the docs.

JSI

JSI is the foundation of the New Architecture, a C++ API for interacting with any JS engine. In contrast to the bridge, which was asynchronous, JSI is synchronous, which allows for invoking native functions faster. It lets JavaScript hold references to C++ host objects and invoke methods directly on them. This removes the major overhead of asynchronous communication between JS and native by serializing objects using the bridge.

Fabric

Fabric is React Native’s new concurrent rendering system, a conceptual evolution of the legacy render system. The core principle is to unify more render logic in C++ to better leverage interoperability between platforms. Host Components like View, Text, etc. are now lazily initialized, resulting in faster startups. Fabric allows us to take advantage of the features introduced in React 18.

TurboModules

This is a new way of writing native modules that also leverages the power of JSI, allowing for synchronous and an order of magnitude faster data transfer from native to JS and vice versa.

It is a rewrite of the communication layer between JavaScript and platform native modules like Bluetooth, Biometrics, etc. It also allows for writing native code for both platforms using C++ and introduces the lazy loading of modules to speed up your app startup time.

Bridgeless mode

For the time being, React Native is not removing the bridge completely; it’s still there, allowing developers to gradually adopt new architecture and JSI. However, usage of the bridge needs to be explicitly stated (by importing it), so not yet migrated modules won’t work.

Meta is planning to soon allow apps to run in completely bridge-less mode, which will result in faster app startup due to removing the overhead of loading the bridge every time the app starts.

How to turn on New Architecture

To turn on the New Architecture in your app, you need to update your app to at least React Native 0.68; however, we recommend upgrading to at least React Native 0.71+ because a lot of improvements have been added.

To migrate your app to the New Architecture, follow these steps:

  1. Upgrade your app to at least React Native 0.71+, you can use https://react-native-community.github.io/upgrade-helper/
  2. Check all third-party libraries that your app depends on; the important thing is that all of them need to be migrated! This might be a long-time blocker for a lot of apps out there. Components that are not yet compatible will show a red box — Unimplemented component: <ComponentName> — and you will likely notice them. In that case, please let the library maintainers know about it, as this will speed up the adoption.
  3. [Android] Set newArchEnabled=true in gradle.properties.
  4. [iOS] Run RCT_NEW_ARCH_ENABLED=1 pod install inside the iOS folder.

Benefits of moving React Native app to New Architecture

Now that you know how New Architecture works, let’s go over its benefits.

Performance

Due to the synchronous nature of the new architecture, while communicating with the native side, there will be some performance improvements. The app’s startup time will be significantly reduced as every native module will be lazily-loaded. Once the bridgeless mode is available, it will also remove the overhead of loading the bridge at startup. However, not every scenario proves this; in some of the benchmarks, architecture performance is worse.

Meta’s goal was not to make new architecture X times faster than the old one. Apart from removing major bottlenecks, they wanted to create a new solid foundation, allowing for new capabilities that could not be developed using previous architecture. Migration of the Facebook app took over a year, and they haven’t noticed any significant performance improvements or regressions that are perceivable by the end user. However, this doesn’t mean that performance improvements won’t come in the future. Now that they reworked internals, they have a great foundation to build upon.

Let’s go over some performance benchmarks by Alexandre Moureaux from BAM. Here is a link to the source: https://github.com/reactwg/react-native-new-architecture/discussions/85

Benchmark of rendering 10K views

In this case, new architecture proves more efficient than the old one. Using on average less CPU but more RAM.

Benchmark of rendering 2K Text components

The old architecture is faster in this scenario because of heavier UI thread consumption.

The official response from the React Native team is that their internal benchmarks while rolling out the New Architecture to users were neutral across all React Native surfaces in the Facebook app on both Android and iOS. As stated by Samuel Susla in this discussion thread, “In the last years, we conducted dozens of tests in production on millions of devices to assure performance was neutral.”

So in most use cases, you can expect a neutral performance impact without any performance regressions. And keep in mind that the New Architecture is getting better every single day, with many developers contributing to the repository, so the results may be totally different by the time you are reading this.

Future readiness

New Architecture allows your app to leverage Concurrent React features. This improves UI responsiveness, provides Suspense for data fetching to handle complex UI loading schemes, and ensures your app is ready for any further React innovations built on top of its new concurrent engine introduced in React 18.

Let’s see how we can leverage React18’s startTransition API to prioritize between two state updates. In our example, a button click can be considered an urgent update, whereas the NonUrgentUI can be considered a non-urgent update. To tell React about a non-urgent update, we can wrap the setState in the startTransition API. This allows React to prepare a new UI and show the old UI until a new one is prepared.

In our example, we wrapped setNonUrgentValue in start-Transition and told React that nonUrgentValue is a transition and not so urgent, it may take some time. We’ve also added a conditional backgroundColor. When you run this example, you will see that once you click on the button, the view will retain its old UI, e.g., if we start at value 1, the UI will be green.

Once you click on the button, the Value text UI will be updated, but the UI for the container will remain green until the transition is completed, and the color will change to red due to the new UI being rendered. That’s the magic of React’s concurrent rendering.

To understand it better, assume that wrapping an update in startTransition renders it in a different universe. We don’t see that universe directly, but we can get a signal from it using the isPending variable returned from the useTransition hook. Once the new UI is ready, both universes merge together to show the final UI.

import React from "react";
import { Button, StyleSheet, Text, View } from "react-native";
const dummyData = Array(10000).fill(1);
const NonUrgentUI = ({ value, isPending }) => {
const backgroundStyle = {
backgroundColor: value % 2 === 0 ? "red" : "green",
};
return (
<View>
<Text>Non urgent update value: {isPending ? "PENDING" : value}</Text>
<View style={[styles.container, backgroundStyle]}>
{dummyData.map((_, index) => (
<View key={index} style={styles.item} />
))}
</View>
</View>
);
};
const ConcurrentStartTransition = () => {
const [value, setValue] = React.useState(1);
const [nonUrgentValue, setNonUrgentValue] = React.useState(1);
const [isPending, startTransition] = React.useTransition();
const handleClick = () => {
const newValue = value + 1;
setValue(newValue);
startTransition(() => {
setNonUrgentValue(newValue);
});
};
return (
<View>
<Button onPress={handleClick} title="Increment value" />
<Text>Value: {value}</Text>
<NonUrgentUI value={nonUrgentValue} isPending={isPending} />
</View>
);
};
export default ConcurrentStartTransition;
const styles = StyleSheet.create({
container: {
flexDirection: "row",
flexWrap: "wrap",
},
item: {
width: 10,
height: 10,
},
});

Show the final UI.tsx hosted with ❤ by GitHub

To understand it better, let’s visualize the code snippet we just went through. The image below shows a comparison of when we use startTransition and when we don’t. Looking at the image, we see that React flushes the urgent update right off, which happens due to calling setValue without wrapping it in startTransition.

Next, we see that React shows the old UI (viewed in green) for the UI that depends on the nonurgent updates, which means the updates that are wrapped in startTransition. We also see a Pending text displayed; this is a way for React18 to tell us that the new UI depending on this state is not yet ready. Once it’s ready, React flushes it and we don’t see the Pending text anymore, and the view color changes to red.

On the other hand, if we don’t use startTransition, React tries to handle both updates as urgent and flushes once both are ready. This certainly has a few downsides, such as the app trying to render some heavy UI all at once, which may cause jarring effects for the users. With React18, we can handle this by delaying the updates that are not urgent.

There are some other noticeable features in React18 that you might want to check out by playing with the linked sandboxes from React’s official website. See useDeferredValue and startTransition with Suspense.

Maintenance & Support

The React Native core team is committed to offer support for the 3 latest versions of React Native (you can check the support policy here) and the React core team plans new features built on the concurrent rendering engine. It’s important to not stay behind, as the cost of paying the tech debt, will get higher in time. It’s worth calling out that React Native is no different to any other software project in this regard. Not updating dependencies may not only cause your team to spend more time on this task when it’s unavoidable. It can also expose your app to security vulnerabilities already patched in the upstream.

The React Native team has dedicated capacity to help the community solve their app and library problems regarding new architecture adoption in close cooperation. Although it’s not stable yet, it’s worth considering starting to plan your migration early on, so you can identify the problems and blockers that your app may be facing. 2023 is the best time to try it, evaluate, ask for support, and give feedback.

You can get a head start with fine support from the React Native core engineers in solving your app’s build and runtime issues preventing you from migration. Once the new architecture gets stable and turned on by default, the core team will surely focus on the further development of React Native, and hence the migration support time will be reduced.

Need help with performance? Give us a shout!

If you’re struggling with improving your app performance, get in touch with us. Our React Native development company is an official Meta partner and community leader. We’ve delivered high-quality solutions for dozens of international clients, from startups to enterprises, and created some cool Open Source projects like Re.Pack or Reassure. Now’s the time we help your business grow.

This article was originally published at callstack.com on February 14, 2023.

--

--