React Native Architecture
Last updated
Last updated
React Native was first introduced in 2015 as a solution for developing cross-platform applications with native capabilities using the ReactJS framework. The original design of the platform did not come without its set of flaws and drawbacks, despite the solution gaining strong community support and seeing gradual increase in popularity due to the fame of its Web counterpart.
Originally announced in 2018, the React Native re-architecture is an undergoing effort by the Facebook team to make the platform more robust and address some of the most common issues brought by developers over the years. We'll take a look on how this re-architecture will affect and improve both app performance and development velocity.
In essence, React Native aims to be a platform-agnostic solution. To that extent, the framework's main goal is to allow developers to write Javascript React code while, under the hood, React Native can deploy its mechanisms to transcribe the React reconciliation tree into something interpretable by whatever is the native infrastructure. This means:
Displaying the UI correctly
Accessing native capabilities
Typically, for the Android/iOS ecosystems, the mechanisms in place right now look a little bit like this:Taken from this talk by Lorenzo S. https://t.co/wDaXRvLtlA?amp=1
There are 3 parallel threads running in every React Native app:
The JS thread is where all the JavaScript code is read and compiled, and where most of the business logic of the app happens. Metro generates the js bundle when the app is “bundled” for production, and the JavaScriptCore runs that bundle when the app starts.
The Native thread is responsible for handle the user interface. It communicates with the JS thread whenever there is a need to update the UI, or access native functions. It can be split into Native UI and Native Modules. Native Modules are all armed at startup, meaning that a Bluetooth module will always be bundled in case of use by React Native, even if it's not. It just wakes up when the app needs to use it.
The Shadow thread is where the layout is calculated. It uses Facebook's own layout engine called Yoga to calculate flexbox layouts and send them back to the UI thread.
To communicate between the JS thread and the Native thread, we use the Bridge. Under the hood, this C++ module is mostly built around an asynchronous queue. Whenever it gets data from one of either side, it serializes the data as a string and pass it through the queue, and deserialize it on arrival.
What this means is that all threads rely on asynchronous JSON messages transmitted across the bridge, and these are sent to either side with the expectation (but not a guarantee) that they will elicit a response some time in the future. And there's also a risk of congestion.
A popular example of why this creates performance issues is seen with scrolling huge lists: Whenever an onScroll event happen on the native world, information is sent asynchronously to JavaScript land, but the native world doesn't wait for Javascript to do its thing and send it back the other way. This creates a delay where there’s a blank space before the info appears on the screen.
Similarly, calculating a layout needs to go through many hoops before it can be displayed on the screen, as it needs to go all the way to the Yoga engine before it can be calculated by the native world. And this implies, of course, going through the bridge too.
We can see how sending JSON data back and forth through async serialization creates performance issues, but how else can we make our JavaScript communicate with the native world? This is where JSI comes into play.
The React Native re-architecture will progressively see the deprecation of the bridge in favor of a new element called the JavaScript Interface (JSI). An enabler for Fabric and TurboModules.
The JSI allows for a few exciting improvements, the first one being that JS bundle is not bound to the JSC anymore, it can use any other JS engine. In other terms, the JSC engine can now easily be swapped with other — potentially more performant — JavaScript engines, like V8 for example.
The second improvement is the foundation of this new architecture: "By using JSI, JavaScript can hold reference to C++ Host Objects and invoke methods on them. JavaScript and Native realms will be truly aware of each other."
In other terms, JSI would allow for complete interoperability between all threads. With the concept of shared ownership, the JavaScript code could communicate with the native side directly from the JS thread, and there won’t be any need to serialize to JSON the messages to pass across, removing all congestion and asynchronous issues on the bridge.Taken from this talk by Lorenzo S. https://t.co/wDaXRvLtlA?amp=1
In addition to improving considerably the communication between the different threads, this new architecture also allows for direct control over our native modules. Meaning that we can use native modules when we need them, as opposed to loading them all once at startup. This results in massive performance improvements of startup times.
This new mechanism could potentially also benefit many different use cases. As we have now the power of C++ in our hands, it's easy to see how React Native could be used to target a very large panels of systems.
Over the years, React Native has accumulated a lot of parts that are now outdated, unused or otherwise legacy. With the main objective of cleaning the non-essential parts as well as improving maintenance, the React Native framework is seeing itself cleaned out of some its features. Meaning that core modules like Webview or AsyncStorage, are gradually being taken out of the React Native core to turn them into community-managed repositories.