Yes, it took 6.5 years for the React Native team at Meta to re-write the entire architecture from ground up. That is a huge timeline. But it seems like it is all worth it in the end. In this blog piece, we will deep dive into both the new and old architecture and check how React Native became almost twice as fast with the new one. Let’s first check the old architecture.
Whenever we open a React Native app on our device, our device launches three threads (three processing units) :
- Javascript thread
- Shadow thread
- Main thread
Javasript thread
Whenever we bundle our React Native codebase, it uses a bundler called “Metro” to convert our React code into baseline JavaScript. And when our device starts a Javascript Thread, all those bundled JavaScript gets executed here. In a brief, this is the place where all the React Native magic happens. But to process this JavaScript, we need a JS Engine. Browsers mostly come with the “V8 Engine”. But there is no such engine in our native devices. That is why, with every React Native bundle, we used to ship an engine called “JavaScript Core”. This engine was written in C.
Shadow thread
This thread is responsible for converting our Flexbox layout into a format that our native platforms would understand. It processes the layout dimensions and positions from our Flexbox code, creating a layout tree (React Shadow Tree) that corresponds to the native system’s requirements. It does all this stuff using a layout engine called “Yoga”.
Main thread
This is purely handled by native platforms. It has three jobs : Converting layout (provided by shadow thread) into actual pixels + Handling user events (taps and swipes) + Managing native modules (enabling camera, push notifications).
How old architecture worked
The above explanation of main thread raises a question. If the JavaScript code and user interactions + native modules are managed by different threads (at different levels), then how do we attach our JavaScript methods/functions to the actual buttons and how do we can trigger native modules from our JavaScript world? Before going into those details, we need a mental model and here it goes :
As it is seen on the above mental model, React Native had been using a Bridge to pass messages between the Main thread and JS thread to bring the interactivity and enabling JavaScript code to access native modules.
Here is how it works exactly : Our JavaScript code describes the UI and attaches a unique message for event handlers with the respective UI, then our JavaScript core engine converts all this into serialized JSON and sends it to the main thread. The main thread decodes the JSON and with the help of the shadow thread, converts the UI into Native UI. And when a button tap will happen, the main thread sends back the unique message attached with that button, so that the JavaScript code can perform the event handler functions, then re-describes the UI and sends it back to the main thread through the same bridge. This process is somewhat the same for dealing with Native modules.
But in overall, this Bridge architecture has its negative areas. It lacks with :
A slow and laggy performance : the message passing in the Bridge is asynchronous by-default, because we don’t want the JavaScript or the Main thread to be blocked. And due to this, when we end up in a scenario where both the JS and Main thread are sending messages rapidly at the same time (rendering huge data and processing UI interactions simultaneously), then the Bridge queue gets blocked and we see a black screen for a second or two. This problem actually goes into more depth that requires a longer discussion or live code to explain better. But for now, let’s see how the new architecture solves it.
The new architecture
Replaced the JavaScript Core with Hermes : The Facebook team wrote a new engine from ground up which has cross-platform compatibility and named it Hermes. Hermes (by benchmarks) is almost three times faster than the old engine and allows the new architecture to do both synchronous and asynchronous UI updating.
JavaScript Interface : The above change allows React Native to develop a middleman-like interface called JavaScript Interface (JSI). This is an interface which allows our JavaScript bundle to share memory with two C++ modules that allow our JS bundle to interact directly with the Native runtime.
New Native Renderer (Fabric) : This is one of the two C++ modules that the Facebook team wrote from scratch to make our React Native world directly manipulate the Native UIs, prioritize some events over others, handle multiple UI updates at the same time in different shadow trees, and show the correct UI at 60+ FPS constantly.
New Native Module System (Turbo Modules) : With this in place, our JavaScript code can now synchronously communicate with Native Modules. All the native module functionality we use can now be performed synchronously, which makes things faster by default. Also, since all this is written in C++, one code works for all the platforms. Also, now all the native modules are lazily loaded, which means it will only load when we need it.
Removal of the Bridge : since we don’t need the Bridge anymore, React Native doesn’t ship with that anymore, which makes startup time a lot faster.
Why everything in C++ : because it has lots of native modules built in that work across the operating systems. Implementing modules in C++ allows for more fine-grained memory management and performance optimizations.
Did you know : React Native team at Meta named the new architecture as “Fabric” and then, retrospectively named the old architecture as “Paper”.
Apart from the above changes, there are a lots of other changes that make React Native even closer to React DOM-like behaviors. You can go through the full list in this blog, which is written by the React Native team : https://reactnative.dev/blog/2024/10/23/the-new-architecture-is-here
Thanks for reading this through 🦋