Original address: https://reactnative.dev/docs/architecture-overview
Translator's Foreword:
At present, the beta version of React 18, which the new architecture relies on, has been released. The documentation of the new React Native architecture for the ecological library and core developers has also been officially released. In a recent interview, Kevin Gozali, a member of the React Native team, also mentioned that the new architecture is not officially released. The version is still short of the last step of delayed initialization, and the last step will be completed approximately in the first half of 2022. There are indications that the new architecture of React Native is really coming.
In the follow-up, we will also introduce the use of the new architecture, analyze the principles of the architecture, and explain the practical solutions through the form of the geek time column.
Due to time constraints, please point out any improper translations. The following is the main text.
This document is still being updated and will conceptually introduce how the new React Native architecture works. The target audience includes the developers of the ecological library, core contributors and particularly curious people.
The document introduces the architecture of the upcoming new renderer Fabric.
Fabric
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 at the C++ layer, improve interoperability with host platforms, and unlock more capabilities for React Native. R&D started in 2018 and 2021, and the React Native in the Facebook app uses the new renderer.
This document introduces the new renderer and its core concepts. It does not include platform details and any code details. It introduces the core concepts, original intentions, benefits, and rendering processes of different scenarios.
Glossary:
Host platform: The platform embedded in React Native, such as Android, iOS, Windows, and macOS.
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). 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. There is more information about the React renderer in this article.
The original intention and benefits of the new renderer
The original intention of developing the new rendering architecture is for a better user experience, and this new experience is impossible to achieve on the old architecture. for example:
- In order 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 you to write request data code more intuitively in React.
- Allows you to use React Concurrent interruptable rendering in React Native.
- It is easier to implement server-side rendering of React Native.
The benefits of the new architecture also include code quality, performance, and scalability.
- Type safety: The code generation tool (code generation) ensures the type safety of both 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.
- Shared 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. This means that those host platform libraries that need to synchronize APIs become easier to integrate.
- Performance improvement: The implementation of the new rendering system is cross-platform, and each platform has benefited from the performance optimization that was 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.
- There is less data serialization between JS and the host platform: 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.
Glossary
JavaScript Interfaces (JSI): A lightweight API for JavaScript engines embedded in C++ applications. Fabric uses it to communicate between Fabric's C++ core and React.
Render, submit and mount
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. What follows is the rendering pipeline and its differences in various scenarios.
(Annotation: The original meaning of pipeline is to split the computer instruction processing process into multiple steps, and to speed up the execution of instructions through parallel execution of multiple hardware processing units. The specific execution process is similar to the pipeline in a factory, hence the name. )
The rendering pipeline can be roughly divided into three stages:
- Render: 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.
- Commit: After the React shadow tree is completely created, the renderer will trigger a commit. 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.
Glossary
React Element Trees: The React element tree is created by React in JavaScript. The tree is composed of a series of React elements. A React element is an ordinary JavaScript object that describes what should 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. 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. Host platforms include Android platform, iOS platform and so on. On Android, the host view is the
android.view.ViewGroup
instance, theandroid.widget.TextView
instance, 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 onLayoutMetrics
, andLayoutMetrics
is calculated by the layout engine Yoga. The style and content information of the host view is obtained from the React shadow tree.The various stages of the rendering pipeline may occur in different threads. For more detailed information, please refer to the thread model section.
There are three different scenarios in the rendering pipeline:
- Initial rendering
- React status update
- React Native renderer status update
Initial rendering
Rendering stage
Imagine you are going to render a component:
function MyComponent() {
return (
<View>
<Text>Hello, World</Text>
</View>
);
}
// <MyComponent />
In the above example, <MyComponent />
is a React element. React will simplify React elements into the final React host component. Each time, the function component MyComponet or the render method of the class component will be called recursively until all components have been called. Now you have a React element tree of React host components.
Glossary:
React components (React Component): React components are JavaScript functions or classes that describe how to create React elements.
React Composite Components: The render method of React components includes other React composite components and React host components. (Annotation: Composite components are components declared by developers)
React Host Components: The view of React components is achieved through host views, such as
<View>
and<Text>
. In the Web, the host components of<p>
are the components represented by the 061c8524c46f96 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. Note that <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. This is how the shadow node is assembled.
other details
- The operations of creating a React shadow node and creating a parent-child relationship between two shadow nodes are synchronous and thread-safe. The execution of this operation is from React (JavaScript) to the renderer (C++), and in most cases it is executed on the JavaScript thread. (Annotation: The thread model is explained later)
- The React element tree and the elements in the element tree do not always exist. It is only a description of the current view, which is ultimately implemented by React "fiber". Each "fiber" represents a host component and stores a C++ pointer to the React shadow node. These are all possible because of JSI. To learn more about "fibers", refer to 161c8524c470b7 this document .
- The React shadow tree is immutable. In order to update any React shadow node, the renderer creates a new React shadow tree. In order to make the status update more efficient, the renderer provides a clone operation. For more details, please refer to the React status update section below.
In the above example, the products of each rendering stage are shown in the figure:
Commit 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 position 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 promotion, from new tree to 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 upgrade 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 minimum time unit of CUP)
more details
- These operations are performed asynchronously in a background thread.
- Most layout calculations are executed in C++, and only certain components, such as Text, TextInput components, etc., are executed 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.
Mount 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. Remember, this React element tree looks like this:
<View>
<Text>Hello, World</Text>
</View>
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, it is <Text>
create a text to "Hello World" of android.widget.TextView
instance. iOS is similar, creating a UIView
and calling NSLayoutManager
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.
In more detail, the mount phase consists of three steps:
- Tree Diffing: This step is completely calculated in C++, and it will compare the difference between the "previously rendered tree" and the "next tree". The result of the calculation is a series of atomic change operations on the host platform, such as
createView
,updateView
,removeView
,deleteView
and so on. In this step, the React shadow tree will also be flattened to avoid unnecessary host view creation. The details of the algorithm for view flattening can be found later. - tree promotion, from the next tree to the rendered tree (Tree Promotion, Next Tree → Rendered Tree): In this step, the "next tree" will be automatically promoted to the "previously rendered tree", so in the next In a mounting phase, the tree comparison calculation uses the correct tree.
- View Mounting: This step will perform an atomic change operation on the corresponding native view. This step occurs on the UI thread of the native platform.
more details
- 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.
- When initializing the rendering, the "previously rendered tree" is empty. Therefore, the tree diffing step will only generate a series of change operations that only include creating views, setting attributes, and adding views. In the next React state update scenario, the performance of tree comparison is crucial.
- In the current production environment test, the React shadow tree usually consists of about 600-1000 React shadow nodes before the view is flattened. After the view is flattened, the number of nodes in the tree will be reduced to about 200. On an iPad or desktop application, this number of nodes may be multiplied by 10.
React status update
Next, let's continue to see what the various stages of the render pipeline look like when the React status is updated. Suppose you are rendering the following components when you initialize the rendering:
function MyComponent() {
return (
<View>
<View
style={{ backgroundColor: 'red', height: 20, width: 20 }}
/>
<View
style={{ backgroundColor: 'blue', height: 20, width: 20 }}
/>
</View>
);
}
Applying the knowledge we learned in the initial rendering section, you can get the following three trees:
Please note that the host view background corresponding to node 3 is red , and the host view background corresponding to node 4 blue . Assume that the product logic of JavaScript is to <View>
the background color of the first embedded 061c8524c475d0 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>
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.
Let us continue to explore what happens at each stage of the rendering pipeline when the state is updated.
Rendering stage
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. 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 these 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 for the "previously rendered tree", and for the "new tree".
Note that node 4 is shared between T and Structure sharing improves performance and reduces memory usage.
Commit phase
After React has created the new React element tree and React shadow tree, you need to submit them.
- Layout Calculation: status is updated is similar to the layout calculation for 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 (New Tree → Next Tree): similar to the tree promotion of initial rendering.
Tree Diffing: This step will calculate the difference between the "previously rendered tree" ( T ) and the "next tree" ( ). The result of the calculation is the change operation of the native view.
- In the example above, these operations include:
UpdateView(**'Node 3'**, {backgroundColor: 'yellow'})
- In the example above, these operations include:
Mount phase
- Tree Promotion (Next Tree → Rendered Tree): In this step, the "next tree" will be automatically promoted to the "previously rendered tree", so in the next mount phase, the comparison calculation of the tree is used Is the correct tree.
- View Mounting: This step will perform an atomic change operation on the corresponding native view. In the above example, only view. 3 (View. 3) background color is updated, turns yellow.
React Native 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 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++ status update is performed, a piece of code will set the C++ status of (N) S . The React Native renderer will repeatedly try to get the latest submitted version of N S , and submit the new shadow node 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. This can prevent conflicts and competition from 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. The detailed steps have been mentioned before.
Cross-platform implementation
React Native renderer uses C++ core rendering to achieve cross-platform sharing.
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 React Native team plans to add the animation system to the rendering system and extend the rendering system of React Native to new platforms, such as Windows, game consoles, TVs, and so on.
There are several advantages to using C++ as the core rendering system. First, a single implementation reduces development and maintenance costs. Secondly, it improves the performance of creating React shadow trees. At the same time, on Android, because JNI for Yoga is no longer used, it reduces the overhead of the Yoga rendering engine and improves the performance of layout calculations. Finally, the memory occupied by each React shadow node in C++ is smaller than that in Kotlin or Swift.
Glossary
Java Native Interface (JNI): An API written in Java for writing native methods in Java. The function is to realize the communication between the C++ core of Fabric and Android.
The React Native team also used the mandatory immutable C++ feature to ensure that shared resources during concurrent access will not be problematic even if they are not protected by locks.
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 for serializing data ByteBuffer
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:
- (i) communicate with React
- (ii) to communicate with the host platform
About the (i) React and the renderer, including rendering (render) React tree and monitoring event (event) , such as onLayout
, onKeyPress
, touch, etc.
About (ii) React Native renderer and the host platform, including the mount host view, including the create, insert, update, delete host view, and monitor the Event .
View flat
View Flattening (View Flattening) is an optimization method for the React Native renderer to avoid too deep layout nesting.
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 "only participates in the layout" 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.
For a very common example, the "participate in layout only" view in the example causes a performance loss.
Imagine you want to render a title. You have an application that has ContainerComponent
. The child component of the container component is a 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:
Note that view 2 and view 3 are "layout only" views, 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.
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.
Thread model
React Native renderer distributes rendering pipeline tasks among multiple threads.
Next, we will define the thread model and provide some examples to illustrate the thread usage of the rendering pipeline.
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.
The renderer uses three different threads:
- UI thread (main thread): the only thread that can manipulate the host view.
- JavaScript thread: This is where the rendering phase of React is executed.
- Background thread: a thread dedicated to layout.
Let's review the execution scenarios supported by each stage:
Render 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.
Background thread batch update from 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. For more details, please refer to React Native Renderer Status Update.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。