React Native is a framework for building native mobile applications using JavaScript and React. Android and iOS apps written using React Native leverage the platforms’ user interface elements to ensure that they look and feel like native apps.
In this post, we will look at the similarities and differences between React and React Native, how the two systems are converging architecturally, and the implication from a performance perspective.
React Native is like React
The business logic for these apps is written in JavaScript and React, and is executed on a JavaScript Virtual Machine that runs on a separate thread. Like a React Web Application, the user interface is declaratively constructed using React component trees. Instead of using DOM primitives like a “div†or a “spanâ€, the leaf nodes in a React Native component tree use “View†and “Textâ€. Consequently, when the React reconciler runs, commands to create a node or set attributes are directed towards a UIView
or an android.View
instead of the standard DOM elements.
… but on a separate thread (today)
Unlike the Web however, the JavaScript code is not executed on the UI thread, but on the background thread where the JavaScript VM runs. This thread separation leads to interesting consequences, one of which is ensuring that the UI thread is smooth and not slowed down by any expensive computations happening in JS code. For example, a view will continue to scroll smoothly even if an unrelated part of the application is rendering a very complex user interface. The communication between the two threads is non-blocking and asynchronous. Today, React Native uses a “bridge†to send queued UI operations to the UI thread.
This asynchronous nature of React Native also benefits its layout system and is great for performance. Styling in React Native is similar to the Web and specified using Flexbox. Since Android and iOS do not natively understand Flexbox, React Native bundles Yoga, a layout system that outputs the relative co-ordinates of components, given flexbox input styles. Just like the JS code, this layout computations can also be performed in a separate non-blocking thread.
Here is a diagram illustrating the three threads, and how React Native works today.
React Native’s multi-threaded architecture
The best analogy of such a system on the web could be an experiment (https://github.com/web-perf/react-worker-dom) where we could run React and JS in a background thread using Web Workers and then asynchronously update the main thread. Like React Native, Web Workers also communicate with the main thread asynchronously and would be non-blocking, which would potentially be good for performance.
Multi-threading is not always good
In most cases, running complex computations on a separate thread could help keep the main UI thread free and responsive. However, there are cases where such multi-thread could get in the way of a smooth user experience. Here are a couple of examples.
- Just like the render commands are sent over to the UI Thread, all events from user interactions will be received on the UI thread and would need to be sent back to the JS VM for processing. The communication is asynchronous and consequently, so is processing the event in the JS code. As a result, we cannot synchronously cancel events, or prevent the default action.
- UI controls like a RecyclerView or UICollectionView virtualize long lists for performance. When it is time to render the “nth-element†based on a scroll position, the API expects a synchronous return value to render a specific element. In more abstract terms, integrating React Native with UI layout systems that expect synchronous rendering is harder today. While React Native may not prevent scrolling long lists, it would still display no content if such lists are really long, and are scrolled really fast.
The above are also the reasons why running React in a Web Worker may not be worth the complexity.
Synchronous and Asynchronous
While React Native today only supports asynchronous communication, there is clearly a case to support both types of rendering for performance. The new architecture of React Native is aimed to also enable the case of synchronous communication. Instead of using a bridge and piping all the commands over a queue, the new communication API is closer to the web.
One the web, most DOM manipulations happen using synchronous APIs like:
var el = document.createElement('div'); el.setAttribute('style', ''); root.appendChild(el);
In the example above, the call to createElement
returns a native, platform specific object. JavaScript can continue to work with that “opaque†object, and add it to the DOM tree. The new React Native API uses a similar JavaScript Interface (also called JSI) to expose C++ objects to the JavaScript world. Hence, all of React’s commands to createNode
, or mountNode
can be direct, performant calls, like in the browser. Depending of the event, this will also allow up to run JavaScript code either on the main thread when responding to events like a scroll, or on a background thread when rendering a page returned from network.
This new architecture will also be able to leverage React Fiber, that would allow clever scheduling of UI updates. More information about this new architecture and its rationale can be found in the ReactConf 2018 session. This convergence is similar to how the Web is starting to embrace the concept of Worklets.
Conclusion
While React and React Native currently render to two different types of surfaces, they share many common UI patterns. The new architecture of React Native brings rendering to mobile devices closer to how its done on the web, paving a road for the future where we may be able to share even more code between web and mobile platforms. While each system has a certain set of APIs and limitations, we are able to learn from the performance patterns of one system and apply it to the other.