React Native: Scalability
When we talk about scalability of React Native, we talk about how much a developed app can handle. This could be, for example, the response time of the app, the amount of memory that the app can handle or what the battery drainage of the app is.
Identification of the system’s key scalability challenges under a plausible scenario.
The moderators of React Native state on their website that performance is a serious issue in React Native. They aim to achieve 60 fps1. But sometimes, this is not reachable. Since the development is meant for multiple platforms, they cannot optimize the performance like a Native app. The problem with React Native, is that it runs on a JavaScript thread. For every component reloading, the app must call and build the native components. When reloading a component with a large component subtree,this may cause stutter and the user might not have a smooth UI feel. The Navigate command might cause such a stutter, since it has to reload a whole new page with new components.
React Native used to have a component called ListView.
This component handled the display of vertically scrolling lists.
The problem with this component was that it tried to render every item in the list, even if that item was not in de window.
So when using a very large list, the app would have to render many components.
With simple text, this is not very cost expensive.
But sometimes, you would like to render a list of images.
This results in a large usage of RAM and would slow the app, since loading takes a lot of time.
In 2017 ListView was replaced by a set of more advanced listing methods such as <FlatList>
2.
As for the battery drainage. React Native does not use an extra amount of energy. The problems for energy are the same as they would be for IOs or Android. Using certain libraries can be quite challenging for your battery3. This includes tracking your location all the time, using network requests or rendering 3D objects.
Empirical quantitative analysis under varying workloads of at least one scalability dimension that can limit scalability. Examples include achievable transactions per second, processing time, memory use, or energy consumption.
To test the workloads of a React Native app, we are going to create an app and use its developer tools. In these developer tools, there is a functionality called ‘Show Perf Monitor’4. In this functionality we can see the following things5:
- What the UI frame rate is.
- What the Javascript frame rate is.
- How much frames you skipped.
Since React Native runs on a Javascript Thread, every event that updates a view will first go through a Javascript event before updating the UI 5. The amount of updates that the Javascript handles is called the JS frame rate. Every update in the native side has a frame deadline. If the Javascript thread cannot call the native side in this deadline, the frame is dropped. This makes sure that the UI remains smooth.
The app that we created, contained a list where each element contained a high resolution image. With this approach, we tried to ‘overload’ the app and to make the app drop frames. However, when scrolling through this list. we could not see any stuttering or errors. The UI frame rate was steady at 60 fps. The JS frame rate dropped to 20fps and only 9 frames were dropped. As we can see, React Native can handle a lot of processing and still makes the ui run smooth.
Old & New Architecture
The architecture of React Native has played a large role in the performance
of apps built with it.
React Native serves as an abstraction layer for mobile development for Android and iOS.
This abstraction, however, comes at the cost of great performance, because accessing
native OS features has to go through a communications bridge between the JavaScript
thread and the Native thread6.
In some way this is inevitable when offering a platform agnostic interface.
When a React Native app is run on a device is consists of three threads:
- the JavaScript Thread;
- the Native Thread;
- and the Shadow Thread.
When an app is loaded on a device it runs in its own native thread, that executes
native code.
In general, apps written with React Native contain relatively little native code
and mostly JavaScript.
Hence, a second thread is started from the Native Thread that runs all the JavaScript
code.
This JavaScript Thread is what will handle most of the apps functionality, but it
can not directly execute system level function calls, as the JavaScript code is
not able to directly commincate with the native code7.
In order to make this possible there is a bridge between the two threads.
Currently, this indirect way of communication is the biggest bottleneck for
performance8.
Lastly, the Shadow Thread is responsible for calculating the lay-out of the UI.
The performance issues did no go unnoticed by the React Native development team
and since several years a new architecture has been in the works,
aptly named The New Architecture.
With this new architecture, the old bridge will be phased out and replaced by the
new JavaScriptInterface (JSI)7.
JSI will be able to let the JavaScript Thread execute some native methods directly in the
Native Thread, without intervention from a bridge.
And whereas the old bridge handled the communication in an asynchronous way, JSI
can offer synchronisation for smoother performance9.
Another improvement that it offers is that with JSI the JS Thread does not have
to run with the old JavaScriptCore engine, but can now run with other enginges as well9.
It will also enable the introducation of new architectural components
to further speed up things.
These are Fabric, CodeGen and Turbo Modules.
Fabric is the new rendering system and comes with a slate of new possibilities.
Some of the most important ones are support for multi-priority and synchronous events,
concurrent features and easier implementation of server side rendering10.
CodeGen plays an important role in allowing communication between the JS and Native thread.
As JavaScript is dynamically typed and C++ is statically typed, CodeGen can type check
the JavaScript code.
This enables that more code is generated at build time instead of run time9.
Finally, Turbo Modules are a new way to use Native modules.
With the old architecture, all native modules had to be loaded up front.
Modules that would never be used would then be loaded as well, increasing load times.
With Turbo Modules, it is possible to load these modules only when they are actually
being used9.
All in all these architectural changes will bring many performance improvements
to React Native and the apps built with it.
How the architectual changes address the identified scalability issues.
The identified challenge was the JavaScript Thread, where, with each component reloading, it must call and build the native components. Here the bottleneck was that when reloading a large component with many sub-components, a lot of updates to the native thread had to be made. For this problem, React Native has updated its architectural design in the new release of version 0.67. This version has some extra components, namely, Fabric, CodeGen, TurboModules, and JSI. The JSI is the most significant update to the architectural design, which will allow the Javascript Thread to directly communicate with the Native side 11. The biggest improvement is the removal of the requirement to serialize or deserialize all the information as JSON for the communication between the two worlds 11. The methods can be written in C++ entirely, which is a lot faster than the previously used method. Furthermore, the methods can be fully synchronous, which means using async/await is no longer required. Furthermore, Fabric allows for the UI manager to create the Shadow Tree (in the Shadow Thread), directly in C++, which again greatly increases the swiftness of the process 12. On top of that, the Turbo Modules allow the javascript code to load each module only when it’s really needed, during run-time 12. This also means that it does not have to send all of the data over the bridge anymore at startup. All in all, the new architectural design greatly improves the bootup time for applications with many modules, decreases the congestion that sometimes happened on the bridge, and overall allows for a more intuitive design of the applications of the future.
-
https://reactnative.dev/blog/2017/03/13/better-list-views ↩︎
-
https://www.callstack.com/blog/optimize-battery-drain-in-react-native-apps ↩︎
-
How the React Native Bridge works (and how it will change in the future) https://www.youtube.com/watch?v=TU_kTuz2i9Y ↩︎
-
https://medium.com/coox-tech/deep-dive-into-react-natives-new-architecture-fb67ae615ccd ↩︎
-
https://blog.notesnook.com/getting-started-react-native-jsi/ ↩︎
-
https://formidable.com/blog/2019/fabric-turbomodules-part-3/ ↩︎