How react-native became performant as native with the new architecture

Taha Ouarrak
OCP digital factory
9 min readNov 17, 2022

How Current Architecture works under the hood?

In a nutshell:

Each react native app is composed of two main parts, the JavaScript code and the native code. The code is executed over three threads:

1) The JavaScript thread: to run JS Bundle with specific JavaScript engine

2) The Native/UI thread: it runs the native code and handles any user interface operation like rendering or gesture events.

3) The Shadow thread: which calculates the position of native elements in the layout.

The relationship between the JavaScript and Native threads is mediated by a component called the Bridge.

Drawbacks

Although React Native apps are fast enough, they still lag behind native solutions in terms of performance. The reason lies with the architecture used.

The bridge in the old architecture works by serializing all data that must be passed from JavaScript to native.

The Bridge have some limitations:

  • It is asynchronous: one layer submits data to a bridge and waits for the data to be processed by the other layer, even when it’s unnecessary.
  • It is single threaded: JavaScript used to be single-threaded, so computation that takes place has to be performed on that single thread.
  • Overhead costs: each time one layer communicates with another, it must serialize the data. The other layer must deserialize it. JSON was chosen for its simplicity and human readability, despite being a lightweight format it has a cost associated with it.

The new architecture

The New Architecture dropped the concept of The Bridge in favor of the JavaScript Interface (JSI), with new components, which increased performance (the new architecture is available starting from RN 0.68).

RN new architecture components

JSI: It will replace the bridge from the current architecture and provide a direct, native interface to JavaScript objects and functions. This idea unlocks several benefits:

  • Synchronous execution: the capability to execute synchronously those functions that should not have been asynchronous in the first place.
  • Concurrency: JavaScript can invoke functions that are executed on different threads.
  • Lower overhead: The New Architecture does not need to serialize or deserialize data anymore, which eliminates the need for serialization taxes.
  • Code sharing: The introduction of C++ has enabled the development of platform-independent code, which can be shared easily between platforms.
  • Type safety: To ensure that JavaScript can properly invoke methods on C++ objects, a layer of code has been added to the language. This code is generated starting from some JavaScript specifications that must be typed through Flow or TypeScript.

Fabric: the UIManager that will be responsible for the native side. The difference now is that instead of communicating with JavaScript through a bridge, Fabric exposes its functions via JavaScript so the JS side and vice-versa can communicate directly through ref functions. passing data between sides will be performant.

Turbo Modules: These modules are much like native modules in the current architecture, but they are lazily-loaded when they are needed instead of loading all of them at the launch time. They are also exposed using the JSI so JS holds a reference to use them on the JavaScript side, resulting in better performance, especially on the launch time.

CodeGen: it makes JavaScript a single source of truth, which will help create static types for the JavaScript so that native side (Fabric and Turbo modules) will be aware of them. This will avoid validation problems and result in better performance, fewer mistakes when passing data, as well as minimizing time consumption.

The flow of new architecture:

  1. The user opens the App.
  2. Fabric directly loads the native side (no native modules).
  3. It tells the JS thread that it is ready and now the JS side loads the main.bundle.js which contains all js and react logic + components.
  4. JS called through the ref native function (the one that was exposed as an object using the JSI API) to Fabric and the shadow node creates the tree.
  5. Yoga does the layout calculation converting from flexbox based style to host layout.
  6. Fabric shows the UI.

JavaScript Interface(JSI)

As discussed before, the bridge is going to be replaced with a lightweight general-purpose JavaScript interface, written in C++, that can be used by the JavaScript engine to directly invoke methods in the native realm.

What does the term “general-purpose” mean?

The current architecture uses the JavaScriptCore Engine. The Bridge is only compatible with this particular engine, however the new architecture will decouple the interface from the Engine, enabling the use of other JavaScript Engines like Chakra, v8 and Hermes, etc.

How can JavaScript call native methods with JSI?

Through the JSI, Native methods will be exposed to JavaScript via C++ objects. These objects can be referenced by JavaScript code, thus can be invoked directly. This is similar to the web, where JavaScript code can reference any DOM element, and call methods on that element.

For Example: when you write:

const container = document.createElement(‘div’);

Here, the container is a JavaScript variable that holds a reference to a DOM element. When we call any method on the “container” variable, it will in turn call the method on the DOM element. Similarly, JSI works in the same way.

The JSI will allow JavaScript code to hold a reference to Native Modules, so JavaScript can call methods on this reference directly

To Sum it up, JSI will allow using other JavaScript engines and will allow for complete interoperability between the threads.

One of the main advantages of the JSI is that it’s written in C++. This means that React Native can target a wide range of devices, including smart TVs, watches and other devices.

Fabric

Fabric is a new rendering system for React Native, seeking to improve the interoperability of the framework with host platforms, Improving communication between JavaScript and the native threads.

Improved interoperability between React Native and host views

A host view is a tree representation of the UI elements in the host platform.

The C++ core shared by different host platforms improves interoperability between React Native and the host and enables React Native to render surfaces synchronously, which. In the legacy architecture, the layout was asynchronous, causing a layout “jump” issue when embedding a React Native rendered view.

Improvements in data fetching

Data fetching in applications has become easier with the integration of React Suspense. Other new features available in React 18 are now enabled in the Fabric renderer, such as Concurrent Features, which keeps the UI of our applications responsive during expensive state transitions.

The Fabric render pipeline

The render pipeline occurs in three phases:

  1. The Render phase
  2. The Commit phase
  3. The Mount phase

Let’s look into each of them more closely.

The Render phase

In this phase, React executes code to create React element trees. A React Element is a plain JavaScript object that describes what appears on the screen.

The React element tree is used to render the React shadow tree in C++. The Fabric Renderer creates a shadow tree, which is made up of React shadow nodes which represent components to be mounted.

const App = () => {
return (
<View>
<Text>Hello World</Text>
</View>
);
};

During the Render phase, a shadow node is created for each React element. This shadow node is created synchronously, only for React host components, and not for composite components such as <View>.

When transformed into a Shadow Block , the <View> is translated into a <ViewShadowNode> object.

The new renderer will automatically reflect the parent-child relationships between React element nodes. The above process shows how the React shadow tree is assembled; once it is complete, the renderer triggers a commit of the element tree.

Here is a representation of the render phase:

The Commit phase

Cross-platform layout engine Yoga performs operations that occur during the commit phase, which consist of two operations: layout calculation and tree promotion.

The layout calculation generates the position and size of each React shadow node by invoking Yoga to calculate its layout.

The Tree Promotion operation promotes the new React shadow tree as the next tree to be mounted. This promotion represents the latest state of the React element tree.

The Mount phase

This is the phase in which the React Shadow Tree (which contains the data from the layout calculation) is transformed into a host view tree with rendered pixels on the screen. The Fabric renderer creates a corresponding host view for each React shadow node and mounts it on screen.

The React Shadow Tree, which contains the data from the layout calculation, is transformed into a host view tree with rendered pixels on the screen. The Fabric renderer is responsible for creating host views and mounting them on screen.

Turbo Modules

In the current architecture, all Native Modules used in the app (e.g. Bluetooth, Geo Location and File Storage) must be initialized in the start-up, even if the user does not require one of these modules

Turbo Modules are new versions of Native Modules, which allow JavaScript to hold references to these modules, and then modules are loaded only when required. This will improve start-up time.

The Turbo Native Modules are a further evolution of Native Modules that offer a few extra benefits:

  • Consistent interfaces across platforms
  • Using C++ with another native platform language will help you reduce the amount of work required for porting your app to multiple platforms.
  • Lazy loading modules, speeding-up startup

Codegen

JavaScript is a dynamically typed language, and JSI is written in C++, which is a statically typed language. Consequently, there are some communication issues between the two.

That’s why the new architecture includes a static type checker called CodeGen.

The typed JavaScript code will be used to define the interface elements used by Turbo Modules and Fabric. This way, CodeGen generates more native code at build time instead of run time.

Hermes

Hermes is a JavaScript engine designed to reduce app launch time and precompile JavaScript into efficient bytecode.

Is Hermes a good choice?

Since the introduction of Hermes as an opt-in JavaScript compiling engine in React Native 0.64, the JavaScript engine has seen tremendous support from the React Native developer community, especially since it is more performant.

Hermes is not just good for React Native applications, but also reduces bundle size and load time along with a GUI to visualize the performance metrics of your app during development. This feature helps developers learn how their applications will perform after release.

Does Hermes make React Native faster?

According to research conducted by maintainers of React Native, Hermes is the most performant JavaScript engine for building React Native applications. The study evaluated three metrics (Time to Interactive TTI, binary size, and Memory consumption) to determine which engine performs best.

  • TTI is the duration between when an app is launched and when users can interact with it.
  • Binary size is the size of the bundled React Native application in APK (Android) or IPA (iOS)
  • Memory consumption is the size of memory used when running the app.

Here is a link to the complete article covering the research.

How Hermes improves React Native performance

Let’s discuss the benefits of using Hermes as your JavaScript engine for React Native applications.

  • Precompilation: Hermes precompiles app source code to bytecode before starting up.
  • Faster TTI: Hermes reduces the TTI, resulting in a smooth user experience
  • Smaller app bundle size: The size of applications compiled with Hermes is smaller than those built with other JavaScript engines.

Summary

Here is the new architecture, with all of its changes:

The key highlights are:

  • JSI will replace Bridge
  • Complete interoperability among all threads
  • Web-like Rendering system
  • Time sensitive tasks are executed synchronously
  • Lazy Loading of Turbo Modules
  • Static Type Checking between JS and Native Side
  • Ability to swap the JavaScriptCore with other Engines
  • Hermes as default engine

We can expect that this new architecture will deliver some powerful improvements to React Native.

--

--