At present, the beta version of React 18, which the new architecture of React Native relies on, has been released, and the documentation of the new architecture of React Native for the ecological library and core developers has also been officially released. The React Native team member Kevin Gozali also talked about the new architecture in a recent interview. The official release is still short of the last step of delayed initialization, and the last step will be completed in the first half of 2022. There are indications that the new architecture of React Native is really coming.
Earlier, RN officially announced: Hermes will become React Native's default JS engine . In the article, we briefly introduced the upcoming new renderer Fabric, then we focus on getting to know this new renderer Fabric.
1. Fabric
1.1 Basic concepts
Fabric is the rendering system of React Native's new architecture, which evolved from the rendering system of the old architecture. The core principle is to unify more rendering logic in the C++ layer to improve interoperability with host platforms, that is, to be able to call JavaScript code synchronously on the UI thread, and rendering efficiency is significantly improved. Fabric development began in 2018, and many React Native apps inside Facebook use the new renderer Fabric.
Before introducing the new renderer, let's introduce a few words and concepts:
- Host platform : Platform embedded in React Native, such as Android, iOS, Windows, macOS.
- Fabric Renderer (Fabric Renderer) : The React framework code executed by React Native is the same as the code executed by React on the Web. However, React Native renders the general platform view (host view) instead of DOM nodes (you can think of DOM as the host view of the Web).
After replacing the underlying rendering process, the Fabric renderer makes it feasible to render the host view. Fabric allows React to communicate directly with various platforms and manage its host view instances. The Fabric renderer exists in JavaScript, and it calls an interface exposed by C++ code.
1.2 The original intention of the new renderer
The original intention of developing the new rendering architecture is to better improve the user experience, and this new experience is impossible to achieve on the old architecture. Mainly reflected as:
- To improve the interoperability of host views and React views, the renderer must be able to measure and render the React interface synchronously. In the old architecture, the React Native layout is asynchronous, which leads to the rendering of nested React Native views in the host view, and there will be a problem of layout "jitter".
- With the help of multi-priority and the ability to synchronize events, the renderer can increase the priority of user interaction to ensure that their operations are processed in a timely manner.
- The integration of React Suspense allows developers to organize request data codes more reasonably in React.
- Allow developers to use React Concurrent interrupt rendering function in React Native.
- Easier to implement server-side rendering of React Native.
In addition, the new Fabric renderer also has a qualitative increase in code quality, performance, and scalability.
- Type Safety : Code generation tool (code generation) ensures the type safety of JS and the host platform. The code generation tool uses JavaScript component declarations as the only source of truth, and generates C++ structures to hold props. There will be no build errors due to mismatch between JavaScript and host component props.
- shares C++ core : The renderer is implemented in C++, and its core core is shared between platforms. This increases consistency and makes it easier for new platforms to adopt React Native. (Annotation: For example, the new VR platform)
- Better host platform interoperability : When the host component is integrated into React Native, synchronous and thread-safe layout calculations improve the user experience.
- performance improvement : The implementation of the new rendering system is cross-platform, and each platform gets a better user experience from the performance optimizations that were originally implemented only on a specific platform. For example, the flat view level was originally just a performance optimization solution on Android, but now it is directly available on Android and iOS.
- Consistency : The implementation of the new rendering system is cross-platform, making it easier to maintain consistency between different platforms.
- faster startup speed : By default, the initialization of the host component is performed lazily.
- JS and the host platform is less : React uses serialized JSON to pass data between JavaScript and the host platform. The new renderer uses JSI (JavaScript Interface) to directly obtain JavaScript data.
Second, the rendering process
2.1 Rendering process
The React Native renderer renders React code to the host platform through a series of processing. This series of processing is the rendering pipeline, and its function is to initialize rendering and update the UI state. Next, we focus on the React Native rendering pipeline and its differences in various scenarios.
The rendering pipeline can be roughly divided into three stages:
- rendering : In JavaScript, React executes those product logic codes to create React Element Trees. Then in C++, use the React element tree to create a React Shadow Tree.
- submission : After the React shadow tree is completely created, the renderer will trigger a submission. This will promote the React element tree and the newly created React shadow tree to "the next tree to be mounted". This process also includes layout information calculation.
- mount : After the React shadow tree has the layout calculation result, it will be transformed into a Host View Tree.
Here are a few terms that need to be explained:
React element tree
The React element tree is created by React in JavaScript, and the tree is composed of a series of React-like elements. A React element is an ordinary JavaScript object that describes what needs to be displayed on the screen. An element includes property props, styles, and children. React elements are divided into two categories: React Composite Components and React Host Components, and it only exists in JavaScript.
React shadow tree
The React shadow tree is created by the Fabric renderer, and the tree is composed of a series of React shadow nodes. A React shadow node is an object that represents a React host component that has been mounted, and its property props come from JavaScript. It also includes layout information, such as coordinate system x, y, width and height. In the new renderer Fabric, React shadow node objects only exist in C++. In the old architecture, it exists in the mobile phone runtime stack, such as Android's JVM.
Host view tree
The host view tree is a series of host views, and the host platforms include Android platform, iOS platform, and so on. On Android, the host view is an instance of android.view.ViewGroup, an instance of android.widget.TextView, and so on. The host view constitutes the host view tree like building blocks. The size and coordinate position of each host view is based on LayoutMetrics, and LayoutMetrics is calculated by the layout engine Yoga of React Native. The style and content information of the host view is obtained from the React shadow tree.
The various stages of the React Native rendering pipeline may occur in different threads, refer to the thread model part.
In React Native, there are usually three operations involving rendering:
- Initial rendering
- React status update
- React Native renderer status update
2.2 Initial rendering
2.2.1 Rendering phase
Suppose, one of the following components needs to be rendered:
function MyComponent() {
return (
<View>
<Text>Hello, World</Text>
</View>
);
}
In the above example, <MyComponent />
will eventually be simplified by React to the most basic React host element. Each time the function component MyComponet or the render method of the class component is called recursively, until all components have been called. Finally, a React element tree of React host components is obtained.
Here, there are several important terms that need to be explained "
- React component : React components are JavaScript functions or classes that describe how to create React elements.
- React composite component : The render method of the React component includes other React composite components and React host components. (Annotation: Composite components are components declared by developers)
- React host component : The view of the React component is implemented through the host view, such as
<View>
,<Text>
. In the Web, the host components of<p>
are the components represented by the 061c9253b0b4c0 tag and the<div>
In the process of element simplification, each time a React element is called, the renderer will simultaneously create a React shadow node. This process only happens on React host components, not on React composite components. For example, a <View>
will create a ViewShadowNode object, and a <Text>
will create a TextShadowNode object. And the component we developed, because it is not a basic component, there is no direct React shadow node corresponding to it, so <MyComponent>
does not directly correspond to the React shadow node.
While React creates a pair of parent-child relationships for two React element nodes, the renderer will also create the same parent-child relationship for the corresponding React shadow nodes. The above code, the products of each rendering stage are shown in the figure below.
2.2.2 Submission phase
After the React shadow tree is created, the renderer triggers a submission of the React element tree.
The Commit Phase consists of two operations: layout calculation and tree promotion.
Layout calculation
This step will calculate the location and size of each React shadow node. In React Native, the layout of each React shadow node is calculated by the Yoga layout engine. The actual calculation needs to consider the style of each React shadow node, which comes from the React element in JavaScript. The calculation also needs to consider the layout constraints of the root node of the React shadow tree, which determines how much free space the final node can have.
Tree lift
From the new tree to the next tree (Tree Promotion, New Tree → Next Tree), this step will promote the new React shadow tree to the next tree to be mounted. This promotion means that the new tree has all the information to be mounted, and can represent the latest state of the React element tree. The next tree will be mounted in the next "tick" of the UI thread. (Annotation: tick is the smallest CUP Time unit).
Moreover, most layout calculations are performed in C++, and only certain components, such as Text, TextInput components, etc., are layout calculations performed on the host platform. The size and position of the text are unique on each host platform and need to be calculated at the host platform layer. To this end, the Yoga layout engine calls the functions of the host platform to calculate the layout of these components.
2.2.3 Mounting phase
The Mount Phase will convert the React shadow tree that already contains layout calculation data into a host view tree that is rendered on the screen in pixel form.
Standing at a higher level of abstraction, the React Native renderer creates a corresponding host view for each React shadow node and mounts them on the screen. In the example above, the renderer <View>
created android.view.ViewGroup instance, is <Text>
created android.widget.TextView instance of text is "Hello World" is. iOS is similar, creating a UIView and calling NSLayoutManager to create text. Then the host view will be configured with attributes from the React shadow node. The size and position of these host views are configured through the calculated layout information.
The mounting phase is subdivided into three steps:
- tree comparison : This step is completely calculated in C++, and the element difference between the "rendered tree" and the "next tree" will be compared. The result of the calculation is a series of atomic change operations on the host platform, such as createView, updateView, removeView, deleteView, etc. In this step, the React shadow tree will be refactored to avoid unnecessary host view creation.
- tree promotion, from the next tree to the rendered tree : In this step, the "next tree" will be automatically promoted to the "previously rendered tree", so in the next mount stage, the comparison calculation of the tree is used Is the correct tree.
- view mount : This step will perform an atomic change operation on the corresponding native view. This step occurs on the UI thread of the native platform.
At the same time, all operations in the mount phase are executed synchronously on the UI thread. If the commit phase is executed in a background thread, it will be executed in the next "tick" of the UI thread during the mount phase. In addition, if the commit phase is executed on the UI thread, then the mount phase is also executed on the UI thread. The scheduling and execution of the mount phase largely depends on the host platform. For example, the current rendering architecture of the Android and iOS mount layers is different.
2.3 React status update
Next, we continue to look at the various stages of the rendering pipeline when the React state is updated. Assume that the following components are rendered during initial rendering.
function MyComponent() {
return (
<View>
<View
style={{ backgroundColor: 'red', height: 20, width: 20 }}
/>
<View
style={{ backgroundColor: 'blue', height: 20, width: 20 }}
/>
</View>
);
}
By initializing the knowledge learned in the rendering part, we can get the following three trees:
It can be seen that the background of the host view corresponding to node 3 is red, and the background of the host view corresponding to node 4 is blue. Assume that the product logic of JavaScript is to change the background color of the first embedded <View> from red to yellow. The new React element tree looks like this.
<View>
<View
style={{ backgroundColor: 'yellow', height: 20, width: 20 }}
/>
<View
style={{ backgroundColor: 'blue', height: 20, width: 20 }}
/>
</View>
At this point, we may have a question: How does React Native handle this update?
Conceptually, when a status update occurs, in order to update the mounted host view, the renderer needs to directly update the React element tree. But for thread safety, both the React element tree and the React shadow tree must be immutable. This means that React cannot directly change the current React element tree and React shadow tree, but must create a new copy of each tree that contains new attributes, new styles, and new child nodes.
2.3.1 Rendering phase
To create a new React element tree with a new state, React has to copy all the changed React elements and React shadow nodes. After copying, submit the new React element tree.
The React Native renderer uses structure sharing to minimize the overhead of immutable features. In order to update the new state of a React element, all elements on the path from that element to the root element need to be copied. But React will only copy React elements that have new attributes, new styles, or new child elements. Any React elements that have not changed due to status updates will not be copied, but will be shared by the new tree and the old tree.
In the above example, React uses the following operations to create a new tree:
- CloneNode(Node 3, {backgroundColor: 'yellow'}) → Node 3'
- CloneNode(Node 2) → Node 2'
- AppendChild(Node 2', Node 3')
- AppendChild(Node 2', Node 4)
- CloneNode(Node 1) → Node 1'
- AppendChild(Node 1', Node 2')
After the operation is completed, node 1'(Node 1') is the root node of the new React element tree. We use T to represent the "previously rendered tree" and T'to represent the "new tree".
Note that node 4 is shared between T and T'. Structure sharing improves performance and reduces memory usage.
2.3.2 Submission phase
After React has created the new React element tree and React shadow tree, you need to submit them, which also involves the following steps:
- Layout calculation : The layout calculation when the state is updated is similar to the layout calculation of the initial rendering. An important difference is that layout calculations may cause shared React shadow nodes to be copied. This is because if the parent node of the shared React shadow node causes a layout change, the layout of the shared React shadow node may also change.
- tree promotion : similar to the tree promotion for initial rendering.
- tree comparison : This step calculates the difference between the "previously rendered tree" (T) and the "next tree" (T'). The result of the calculation is the change operation of the native view.
In the above example, these operations include: UpdateView( , {backgroundColor:'yellow'})
2.3.3 Mounting phase
- tree promotion : In this step, the "next tree" is automatically promoted to the "previously rendered tree", so in the next mount stage, the tree comparison calculation uses the correct tree.
- view mount : This step will perform an atomic change operation on the corresponding native view. In the above example, only the background color of View 3 will be updated to yellow.
2.4 Renderer status update
For most of the information in the shadow tree, React is the only owner and the only source of truth. And all data from React flows in one direction.
There is one exception. This exception is a very important mechanism: C++ components can have state, and the state may not be directly exposed to JavaScript. At this time, JavaScript (or React) is not the only source of truth. Usually, only complex host components use C++ state, and most host components don't need this feature.
For example, ScrollView uses this mechanism to let the renderer know what the current offset is. The update of the offset is triggered by the host platform, specifically the ScrollView component. The offset information is useful in APIs such as React Native's measure. Because the offset data is held by the C++ state, it is derived from the host platform update and does not affect the React element tree.
Conceptually, the C++ status update is similar to the React status update we mentioned earlier, but there are two differences:
- Because React is not involved, the "Render phase" is skipped.
- Updates can originate and occur in any thread, including the main thread.
Commit Phase: When the C++ state update is performed, a piece of code will set the C++ state of the shadow node (N) to the value S. The React Native renderer will repeatedly try to get the latest submitted version of N, copy it with the new state S, and submit the new shadow node N'to the shadow tree. If React performs another submission during this period, or if other C++ status is updated, this C++ status submission fails. At this time, the renderer will retry the C++ status update multiple times until the submission is successful, which can prevent conflicts and competitions with real sources.
The mount phase (Mount Phase) is actually the same as the mount phase of React status updates. The renderer still needs to recalculate the layout and perform tree comparisons.
Three, cross-platform realization
In the previous generation of React Native renderers, the React shadow tree, layout logic, and view leveling algorithm were implemented separately on each platform. The current renderer design adopts a cross-platform solution, sharing the core C++ implementation. The Fabric renderer directly uses C++ core rendering to achieve cross-platform sharing.
Using C++ as the core rendering system has the following advantages.
- A single implementation reduces development and maintenance costs.
- Improved the performance of creating React shadow trees. At the same time, on Android, because JNI for Yoga is no longer used, the overhead of Yoga rendering engine is reduced, and the performance of layout calculation is also improved.
- The memory occupied by each React shadow node in C++ is smaller than that in Kotlin or Swift.
At the same time, the React Native team also used the mandatory immutable C++ feature to ensure that shared resources during concurrent access are not protected even if they are not locked. But there are two exceptions on the Android side, the renderer still has JNI overhead:
- Complex views, such as Text, TextInput, etc., still use JNI to transfer property props.
- JNI will still be used to send changes during the mounting phase.
The React Native team is exploring a new mechanism using ByteBuffer to serialize data to replace ReadableMap and reduce the overhead of JNI. The goal is to reduce the overhead of JNI by 35-50%.
The renderer provides an API for C++ to communicate with both sides:
- Communicate with React
- Communicate with the host platform
Regarding the communication between React and the renderer, including rendering the React tree and monitoring events, such as onLayout, onKeyPress, touch, etc. The communication between the React Native renderer and the host platform includes mounting host views on the screen, including create, insert, update, and delete host views, and monitoring events generated by users on the host platform.
Fourth, the view is flat
View Flattening is an optimization method for React Native renderer to avoid too deep nesting of layouts. React API is designed to achieve component declaration and reuse through composition, which provides a good model for simpler development. But in the implementation, these features of the API will cause some React elements to be deeply nested, and most of the React element nodes will only affect the view layout, and will not render any content on the screen. This is the so-called "participate in layout only" type node.
Conceptually, the relationship between the number of nodes in the React element tree and the number of views on the screen should be 1:1. However, rendering a deep "layout only" React element will slow performance. Suppose there is an application that has a container component with a margin ContainerComponent. The child component of the container component is the TitleComponent. The title component includes a picture and a line of text. The React code example is as follows:
function MyComponent() {
return (
<View> // ReactAppComponent
<View style={{margin: 10}} /> // ContainerComponent
<View style={{margin: 10}}> // TitleComponent
<Image {...} />
<Text {...}>This is a title</Text>
</View>
</View>
</View>
);
}
When React Native renders, it will generate the following three trees:
Views 2 and 3 are views that "only participate in layout" because they are rendered on the screen to provide a margin of 10 pixels.
In order to improve the performance of the "participate in layout only" type in the React element tree, the renderer implements a view flattening mechanism to merge or flatten such nodes, reducing the depth of the host view on the screen. The algorithm takes into account the following attributes, such as margin, padding, backgroundColor, opacity, and so on.
The view leveling algorithm is part of the diffing phase of the renderer. The advantage of this design is that we don't need additional CUP time-consuming to level the views that "only participate in layout" in the React element tree. In addition, as part of the C++ core, the view-leveling algorithm is shared by all platforms by default.
In the previous example, view 2 and view 3 will be flattened as part of the "diffing algorithm", and their style results will be merged into view 1.
However, although this optimization allows the renderer to create and render two host views less, there is no difference in the screen content from the user's perspective.
Five, thread model
The React Native renderer is thread-safe. From a higher perspective, thread safety in the framework is guaranteed by immutable data results, which uses the const correctness feature of C++. This means that every update of React in the renderer will recreate or copy new objects instead of updating the original data structure. This is the premise for the framework to expose thread safety and synchronization APIs to React.
In React Native, the renderer uses three different threads:
- UI thread : the only thread that can manipulate the host view.
- JavaScript thread : This is where the React rendering phase is executed.
- background thread : a thread dedicated to layout.
The following figure describes the complete process of React Native rendering:
5.1 Rendering the scene
Render in a background thread
This is the most common scenario, and most of the rendering pipeline occurs in JavaScript threads and background threads.
Render in the main thread
When there are high-priority events on the UI thread, the renderer can execute all rendering pipelines on the UI thread synchronously.
Default or continuous event interrupt
In this scenario, a low-priority event of the UI thread interrupted the rendering step. React and React Native renderers can interrupt the rendering step and merge its state with a low-priority event executed on the UI thread. In this example, the rendering process will continue to be executed in a background thread.
Irrelevant event interruption
The rendering step is interruptible. In this scenario, a high-priority event of the UI thread interrupts the rendering step. React and the renderer can interrupt the rendering step and merge its state with high-priority events executed by the UI thread. The rendering steps in the UI thread are executed synchronously.
Batch update from the background thread of the JavaScript thread
Before the background thread dispatches updates to the UI thread, it checks whether there are new updates from JavaScript. In this way, when the renderer knows that the new state is coming, it will not directly render the old state.
C++ status update
The update comes from the UI thread and will skip the rendering step.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。