React Native's New Renderer: Fabric

React Native's New Renderer: Fabric

Featured on Hashnode

At the 2018 React Conference, the React Native team announced a few changes to its core architecture and a timeline for how this revamp of its inner working code would proceed. One of the changes announced was the new UI manager, called Fabric. Fabric is the React Native forerunner to the old legacy renderer. Made to “improve interoperability with host platforms, and to unlock new capabilities for React Native”, Fabric takes a different approach to how UI view instances will be created in a React Native application.

The React Native selling point has always been about creating truly platform-specific native UIs and not relying on web views like other cross-platform frameworks. With built-in access to the host platform's native APIs, you can create applications that behave consistently and efficiently across all host platforms. The new Fabric architecture changes none of this but instead builds on it, making the experience even more platform specific. Let's dive into the old legacy renderer to understand better how Fabric works, the fundamental changes, and why these changes are necessary.

The Old Architecture: Legacy Renderer

When your application starts up, it calls the native components in your code, while React talks to the existing UI manager(in this case, the legacy renderer) to create the different UI elements. The UI manager consists of three threads that handle the various stages of rendering your React Native code into views on the host platform: the Js thread, the shadow thread, and the main thread.

React Native Threads.jpg The Javascript thread handles all the business logic in your code. This thread is where all React code will be executed. The Shadow Thread is responsible for calculating layout operations such as relative positions, e.t.c., on the host platform. Because mobile devices don't understand Flexbox styles like the web, React Native uses the Yoga library, a React layout engine, to translate Flexbox layouts to the host platform. Once all layout operations are over, the host platform's main thread renders all this on the host View.

To communicate between the native and Javascript realms, React Native uses a bridge architecture. Suppose we want to create a view on a device; React Native will have to parse a create view command into a JSON array, have it serialised as a string and then pass it over the bridge for the native platform to execute.

You can read this article on Understanding the React Native bridge concept to get a more in-depth dive.

It's important to note that all operations in the old system using the bridge concept are asynchronous.

Because of this async system, rendering animations and events like scrolling in your application can seem laggy due to time spent passing data from the JS thread, to calculating the layout before any actual rendering occurs. These are just milliseconds spent, but the effect can be noticed on the user end.

The New Architecture: Fabric

A significant problem with the old system, specifically the bridge, is that all operations are asynchronous. Also, since communication between the three threads is done over the bridge, passing data becomes slow and cumbersome. For Fabric to work, React Native introduced the Javascript Interface(JSI) as a new system for communication between Javascript and the native realm. JSI exposes native modules directly to JavaScript via C++ and holds a reference to the host object. This system allows you to call methods on the host platform using native code statically.

This is very similar to how the web works, where JavaScript can hold a reference to a DOM element and call methods on it. Example: document.getElementById() JSI allows for both asynchronous and synchronous actions. A task like an API call can be executed asynchronously, while other functions like animations that require a synchronous process can be treated as such.

JSImodule-organization-new.png Image source @ se.ewi.tudelft.nl/desosa2019/chapters/react..

JSI replaces the bridge architecture and solves many of its problems.

Fabric was created to be a cross-platform solution by sharing a fundamental C++ implementation. This improves development, performance, and maintenance on all host platforms. Now let us understand what exactly happens when rendering a UI element on a device with Fabric.

Fabric Render Pipeline

The series of stages Fabric must pass to render React logic on a host platform is called the render pipeline. Ideally, there are three stages: render, commit, and mount.

Stage 1 - Render

function MyComponent() {
  return (
    <View>
      <Text>Hello, World</Text>
    </View>
  );
}

Suppose we were to render the above component.

When our app runs, React creates a React element tree in Javascript; from this tree, Fabric will make an immutable React shadow tree in C++. Inside the shadow tree are shadow nodes that can be likened to browser DOM nodes. The shadow nodes are host objects created synchronously representing host components like a View. In our example code, the <View> and <Text> elements would create a view shadow node and text shadow node, respectively.

For state updates, considering the shadow tree and nodes are immutable to maintain thread safety, React will create a clone of the current tree with its existing nodes and add all the changes to the new clone.

Note: Fabric will only clone a node affected by an update directly or as a child element. Any unchanged node is shared by the old and new tree on the new render.

Stage 2 - Commit

When the render phase is complete, the app will then proceed to use Yoga to calculate the updated layout metrics of the elements in the shadow tree. In our case, it would include the relative positions and size of the text shadow node and view shadow nodes. The New shadow tree is promoted on to become the Next shadow tree(The tree to be rendered).

Stage 3 - Mount

The mount happens on the main thread and involves React Native taking the existing Next shadow tree and transforming it into a host tree seen as the UI on the users' side. An operation referred to as Tree Diffing computes the changes between the previously rendered tree(If any has been rendered as opposed to an initial render) and the Next shadow tree. The <View> and <Text> are created and mounted on the host platform. A third and final promotion occurs from Next tree to Rendered tree.

A visible View and Text will be shown on the device screen at this stage.

Our <View> element becomes a viewGroup or UIView on Android/IOS platforms. Remember that all of this is synchronously executed.

render pipeline.jpg Stages of the Render Pipeline

Takeaways

  • The bridge architecture has been replaced by the Javascript interface(JSI).
  • Improved interoperability between the native and Javascript thread allows for both synchronous and asynchronous operations.
  • Fabric improves rendering performance on a host platform.
  • Since Fabric is implemented with core C++ and is cross-platformed, adoption and maintenance are much easier on new platforms.

Fabric is currently under active rollout and is being implemented as of the time of this post. The React Native official docs have already been updated to replace the old legacy renderer.