Back

React Native New Architecture

MD Rashid Hussain
MD Rashid Hussain
Oct-2024  -  5 minutes to read
Image source attributed to: https://qubited.com

JavaScript cannot execute without a runtime environment. The runtime environment provides the necessary APIs to interact with the underlying system. In the case of the browser, the runtime environment is the browser itself. In the case of Node.js, the runtime environment is the Node.js runtime. In the case of React Native, the runtime environment is the React Native runtime.

This runtime has a Javascript engine (like v8 as in Google Chrome or spiderMonkey in Mozilla Firefox etc.) which executes the Javascript code and handles memory. The runtime also provides apis to manipulate the DOM, update queues, event loop etc. for synchrnisation within the runtime. This runtime implements the browser standards, the web APIs etc. and provides a way to interact with the underlying system.

React Native earlier had JSC (Abbreviation for JavasciptCore). It is a c-based javascipt engine that powers the Safari browser. Since, there was no direct ways to communicate with the JSC due to language and underlying API constrains, react-native used a bridge for serialization and deserialization of data to communicate with the JSC.

Serialization is a process to convert a language's native data structure to an open format, most preferably JSON, and vice-versa for deserialization.

The react-native bridge would convert all Javascript events to JSON to be able to be used by the JSC, and the response would be deserialized back to JSON to be sent to the Javascript world to facilitate this Inter-process communication (IPC).

flowchart LR React([React]) --> JsEngine[Javascript Engine] Yoga[Yoga<br>Shadow/Background Thread] subgraph JsEngine [Javascript Engine] JsBundle[Js Bundle] end subgraph NativeUI[Native UI/Main Thread] NativeModulesSub[Native Modules] end Bridge[Bridge] JSONAsync[JSON - Async] --> Bridge NativeModules[Native Modules] --> Bridge Yoga <-.-> NativeUI JsEngine <-- JSON --> Bridge Bridge <-- JSON --> NativeUI

Since, this serialization-deserialization can take any amount of time depending on the workload, this call has to be asynchronous. There are some big problems in this design,

  • the JSON serialization and deserialization is a CPU intensive recursive function, also this has to be done for every call, making it a bottleneck in the performance of the application,
  • the bridge is a bottleneck in the performance of the application,
  • the bridge is a single-threaded, so all the calls are synchronous and blocking,

Then comes JSI (Javascript Interface), a C++ interface designed to be lightweight and engine-agnostic. Now, the flow of data changed.

A command would invoke JSI, which can dispatch and invoke commands via C++ through a C++ shared memory layer. The updates in memory would be reflected in the Javascript runtime. So, Javascript can directly call the C++ layer now.

flowchart LR subgraph A[ ] React[React] StaticTypes[Static Types] end subgraph B[ ] JsBundle[Js Bundle] JsEngine[Any Js Engine] JsThread[Js Thread] end subgraph C[Generated Interfaces] JSI[JSI] Fabric[Fabric] TurboModules[Turbo Modules] end subgraph D[ ] NativeUI[Native UI] NativeModules[Native Modules] UiOrNativeMainThread[UI or Native Main Thread] end Yoga[Yoga - Layout Engine] A -- Metro --> B A --> CodeGen[Code Gen] CodeGen <-.-> JSI JsBundle <-.-> JSI C <-.-> Yoga JSI <-.-> D

In summary, the overall flow works like this

  • Javascript runtime passes some value to the underlying Javascript engine
  • JSI would track it in C++.
  • Gets passed to the bound host function
  • Convert the JSI value to a C++ dynamic
  • Using the Native Modules, this C++ dynamic is converted to the native type, along with any arguments

In same is the case with rendering UI. The UI is rendered in the native layer, and the updates are passed to the Javascript layer via JSI.

JSI can only be read/written-to from the Javascript thread, otherwise you can encountr unexpected results. The react-native framework is responsible for managing the lifecycle of the program.

we can embed a native function called host function into the Javascript globals and vice-versa. This way, we can share objects between Javascript and native without worrying about any memory management (already handled by the JSI).

  • Since JSI is engine-agnostic, other engines (which maybe more-performant for this task) may be used as the runtime (like the static hermes)
  • You can Interact with the JSI directly from the Javascript thread, so no need to worry about the bridge.
  • Host delegates can be used to manage the lifecycle of the program.

  • There is no bridge involved. So, we should be able to save all those precious CPU cycles (that were earlier used in JSON ser-de). A win in terms of speed
  • Calling native-functions, native-events, lifecycle-events etc. in a more idiomatic way
  • and much more.

The new architecture is a big step in the right direction. It is more performant, more idiomatic and more flexible. It is a big step in the right direction for the React Native framework.

But at the same time, it is a big change. So, it is important to understand the new architecture and how it works. This will help you in debugging, profiling and writing more performant code.

A lot of the features are still in the experimental stage, so it is important to keep an eye on the updates and changes in the React Native framework.