React Native App performance is a myth?
To build cross-platform mobile apps using JavaScript and React, React Native is a popular framework. Leveraging native UI components and APIs enables developers to streamline code across platforms. However, React Native faces challenges affecting performance and developer engagement.
Here we discuss how its updated architecture addresses these issues.
Old Architecture: Bridge and Batched Bridge
React Native’s previous architecture utilizes a bridge mechanism to connect the JavaScript and native environments, managing data serialization, threading, and synchronization. However, this bridge introduces drawbacks and bottlenecks, impacting performance and developer experience.
● JavaScript Thread JavaScript code bundle runs in this thread with the help of a JavaScript engine (JSC, Hermes etc.)
● Native Thread Native UI rendering and handling user events (click, swipe etc.) runs in native thread.
● Bridge these 2 threads run separately in 2 different worlds and don’t know each other’s technology. To make the whole system work there must be some mechanism to talk to each other. The mechanism that helps to communicate these 2 separate threads is called Bridge.
What is the React Native Bridge?
The essential advantage of React Native is that it invokes native UI views on the mobile platform. Due to the bridge technology, it becomes possible to connect the application logic with the UIView manager on the native side.
React Native bridge converts the data to the JSON format. If a user does an action on the mobile screen, the UI thread reports to the main JavaScript thread. It batches the data to a JSON message and sends it over to the JS thread
How do Native and JavaScript Threads communicate?
JavaScriptCore + ReactJS + Bridges becomes React Native
The above diagram shows how JavaScript modules are called or executed.
JavaScriptCore
Responsible for JS code interpretation and executionReactJS
Responsible for description and managementVirtualDom
, directing native components to draw and update, and a lot of calculation logic is also performed in js. ReactJS itself does not directly draw the UI. UI drawing is a very time-consuming operation, and native components are best at this.Bridges
Used to translate ReactJS drawing instructions to native components for drawing, and at the same time feedback user events received by native componentsReactJS
.
As
Bridge
mentioned earlier, the power of RN is that it can connect JS and Native Code, allowing JS to call rich native interfaces, fully utilizing the capabilities of the hardware, and achieving very complex effects while ensuring efficiency and cross-platform functionality.
Bridge
Native code is responsible for managing native modules and generating corresponding JS module information for JS code to call. The encapsulation of each functional JS layer is mainly adapted to ReactJS, making it easier for the functions of the native module to be called using ReactJS.MessageQueue.js
It isBridge
a proxy at the JS layer and all JS2N and N2JS calls will be forwardedMessageQueue.js
through it. There is no pointer transfer between JS and Native, all parameters are passed in strings. All instances will be numbered on both JS and Native sides, and then a mapping will be done, and then the number/string number will be used as a search basis to locate cross-border objects.
The graph just pictures a moment middle of JavaScript execution
The calls have to start from native* it calls into JS, and during the execution, as it calls methods on NativeModules
, it enqueues calls that have to be executed on the native side. When the JS finishes, native goes through the enqueued calls, and as it executes them, callbacks and calls through the bridge (using the _bridge
instance available through a native module to call enqueueJSCall:args:
) are used to call back into JS again.
Java Script to Java calls
MessageQueue.js
, This is where all the communication between the two is being handled. Messages look a bit like JSON-RPC
, passing method calls from JavaScript to native, and callbacks from native back to JavaScript. This is the sole connection between the JavaScriptcontext and the native context. Everything is being passed through MessageQueue, every network request, network response, layout measurement, render request, user interaction, animation sequence instructions, invocation of native modules, I/O operation, you get the idea… Making sure MessageQueue isn’t congested is critical to ensure the smooth operation of our app.
This complete visualization of how a user clicks from native will be processed.
1. User click events from Native UI will be passed to Native to JS call.
2. Business Logic or communication to native devices will be pushed to Js message queue “JS Logic module”
3. All Rendring call to Native module will returned with update value to JS Message Queue
4. For UI updates, Virtual DOM and Reconciliation manage the differences in the DOM, by consuming the events / JS calls from message queue handles delta updates.
What are the performance issues and limitations?
Old Architecture has some limitations:
- Serialization and deserialization overhead: Converting data between formats like JSON, Java objects, and Objective-C objects adds CPU time and memory usage, particularly with large or intricate data structures.
- Asynchrony and latency: Operating in batched mode, the bridge sends messages at set intervals or when the queue reaches capacity. This introduces delays between sending and receiving messages, affecting responsiveness, especially for interactive features like animations and gestures.
- Limitations of native modules: The bridge supports only a subset of native features and types, such as strings, numbers, booleans, arrays, and maps. Consequently, native modules must align their APIs with these constraints, potentially reducing functionality or performance for features requiring more complex or custom types like images, videos, and streams.
- It is asynchronous: one side submitted the data to the bridge and asynchronously “waited” for the other side to process that.
- It is single-threaded: JavaScript side work on a single thread; therefore, the computation in that world had to be performed on that single thread.
- It imposed extra overheads: every time one side had to use the other side, it had to serialize data information. The other side had to deserialize data. The chosen format was JSON for its simplicity and human-readability, but despite being lightweight, it has a cost to pay.
- JSC bundled with Android APK: since the “JavaScriptCore” engine is only present on iOS devices. The JSC engine needs to be part of the Android APK bundle.
- Bridge is Bottleneck: the bridge is a bottleneck for fast communication.
- Native modules must load on start: since both sides do not know each other and their object types. It creates a new type of problem that JavaScript cannot start modules when needed, applications have to load all native modules at the start of the application.
The New Architecture: JSI, Fabric, and Turbo Modules
Prerequisites
- React Native 0.69 — this is the version that introduces the new Architecture.
- Typescript app — Codegen requires that we use types.
Tip: Remove any old versions of
react-native-cli package
, as it may cause unexpected build issues. You can use the commandnpm uninstall -g react-native-cli @react-native-community/cli
JSI (JavaScript Interface)
JSI (JavaScript Interface) was written in C++. It replaced the bridge from the OLD architecture and provided a direct, native interface to JavaScript objects and functions.
The bridge is going to be replaced with a module called JavaScript Interface, which is a lightweight, general-purpose layer, written in C++ that can be used by the JavaScript engine to directly invoke/call methods in the native realm.
Through the JSI, Native methods will be exposed to JavaScript via C++ Host Objects. JavaScript can hold a reference to these objects. And can invoke the methods directly using that reference. This is similar to the web, where JavaScript code can hold a reference to any DOM element, and call methods on it. For Example: when you write:
const container = document.createElement(‘div’);
Here, the container is a JavaScript variable, but it holds a reference to a DOM element which was probably initialized in C++. If we call any method on the “container” variable, it will in turn call the method on the DOM element. The JSI will work similarly.
Unlike the bridge, the JSI will allow JavaScript code to hold a reference to Native Modules. Through the JSI, JavaScript can call methods on this reference directly
Turbo Modules
Turbo Modules is just a Native Module system that replaced the OLD Native Module system.
The New Native module system is also known as the “Turbo Module” which uses JSI technology instead of JSON data serialization. JSI (JavaScript Interface) was written in C++.
In the old architecture, all the Native Modules used by JavaScript (e.g. Bluetooth, Geo Location, File Storage, Camera etc) have to be initialized before the app is opened. This means, that even if the user doesn’t require a particular module, it still has to be initialized at start-up.
Turbo Modules are an enhancement over these old Native modules. As we have seen in the previous part of this article, now JavaScript will be able to hold a reference to these modules, which will allow JS Code to load each module only when it is required. This will significantly improve start-up time for React Native apps.
If an application needs a camera module on the 4th screen of the application, then the application can load the camera module whenever it is required. This on-demand load of native modules helps a lot in the TTI (Time to Interaction) of the application.
- Turbo Module is a new way of implementing “Native modules” in React Native. It implements the Native Module by using JSI & “Native Code” that was generated by Codegen.
- In the New Architecture, Turbo Module introduced Lazy Loading for Native Modules (e.g. Bluetooth, Geo-Location and File Storage) to load when a user launches an app. In OLD architecture all Native Modules used in the app must be initialized in the start-up, even if the user does not require one of these modules
Fabric (New Rendering Engine)
Fabric is the UIManager that will be responsible for rendering the UI in devices. The difference now is that instead of communicating with JavaScript through a bridge, Fabric exposes its functions via JavaScript so the JS side and Native side (vice-versa) can communicate directly through ref functions. passing data between sides will be performant.
The React Native renderer goes through a sequence of work to render React logic to a host (iOS, Android) platform. This sequence of work is called the render pipeline and occurs for initial renders and updates to the UI state.
- Fabric uses JSI to communicate with Hermes and native code, without using the bridge.
- Fabric is a new rendering system for React Native, seeking to improve the interoperability of the framework with host platforms (the platform of Native Side. Ex: Android or iOS), Improving communication between JavaScript and the native threads.
The render pipeline can be broken into three general phases:
Render: React executes code logic which creates React Element Trees in JavaScript. From this React Element Trees tree, the renderer creates a React Shadow Tree in C++.
Commit:After a React Shadow Tree is fully created, the renderer triggers a commit. This promotes both the React Element Tree and the newly created React Shadow Tree as the “next tree” to be mounted. This also schedules calculation (with help of Yoga layout manager) of its layout information.
Mount:The React Shadow Tree, now with the results of layout calculation, is transformed into a Host View Tree.
Codegen (Native Code Generator)
So, C++ is a strongly typed language and JavaScript/TypeScript is not a strongly typed language. To communicate between both JavaScript and C++ there must be some interface. “CodeGen” is a tool that helps to create the interface from the JavaScript/TypeScript that generated interfaces help to communicate between both the C++ world and the JavaScript world.
The “CodeGen” is not a proper pillar, but it is a tool that can be used to avoid writing a lot of repetitive code. Using “CodeGen” is not mandatory: all the code that is generated by it can also be written manually. However, it generates scaffolding code that could save you a lot of time. The “CodeGen” is invoked automatically by React Native every time an iOS or Android app is built.
Codegen especially do these 2 jobs in the new architecture of React Native
Static type checking:
- JavaScript is a dynamically typed (you don’t need to define the type of a variable) language, and JSI is written in C++, which is a statically typed (you must define variable type) language. Consequently, there are some communication issues between the two (JS and C++).
- That’s why the new architecture includes a static type checker called CodeGen. It solves the type-related communication issue between JavaScript & C++.
Generate “Native code”:
Codegen is responsible for generating Native Code for 2 big components of New Architecture. These are:
The Turbo Module component
“typed JavaScript code” to define the Native components or Native modules that you want to use in your app.
The Fabric component
the JavaScript code of the app defines user interface elements using React components, which are pieces of code that describe how a part of the app should look and behave. React components are translated into native code using a feature called Fabric, which is a new rendering system for React Native. Fabric also uses JSI to communicate with JavaScript and native code, without using the bridge. Fabric improves the speed and responsiveness of user interface updates, as well as integrates better with native features.
What are the Benefits of New Architecture?
- Improved performance: JSI eliminates the overhead of serialization and deserialization, reducing CPU time and memory consumption. Fabric enables concurrent rendering, which allows React to render multiple components at the same time without blocking the main thread. Turbo Modules enables synchronous native calls, which reduces latency and improves responsiveness.
- Improved developer experience: JSI allows developers to use any JavaScript engine they prefer, such as Hermes or JSC. Fabric supports all React 18 features, such as Suspense, Concurrent Mode, Server Components, etc. Turbo Modules support more native features and types, such as images, videos, streams, etc., without requiring extra wrappers or adapters.
- Improved compatibility: JSI provides a unified interface for interacting with any JavaScript engine or native platform. Fabric follows the same principles and APIs as React DOM or React Native Web. Turbo Modules follow the same conventions and patterns as JavaScript modules or ES6 modules.
- Synchronous execution: it is now possible to execute synchronously those functions that should not have been asynchronous in the first place.
- Concurrency: it is possible from JavaScript to invoke functions that are executed on different threads.
- Lower overhead: The new Architecture doesn’t have to serialize/deserialize the data anymore; therefore, there are no serialization taxes to pay.
- Type safety: to make sure that JS can properly invoke methods on C++ objects and vice-versa, a layer of code automatically generated has been added. The code is generated starting from some JS specification that must be typed through Flow or TypeScript.
How to Migrate to the New Architecture
The new architecture in React Native is still experimental and under development. However, it is possible to try it out by opting into the new app template in React Native 0.68 or later. The new app template includes extensive comments and documentation to help you get started with the new architecture.
To migrate your existing app or library to use the new architecture, you can follow the migration guide on the React Native website. The migration guide outlines the steps you need to follow to roll out the new architecture across your Android and iOS projects. It also provides examples and best practices for creating custom Fabric components or Turbo Modules.