React Native Architecture: A Conceptual Overview

Kuldeep Singh
8 min readJul 1, 2023

--

React Native is a popular framework for building cross-platform mobile applications using JavaScript and React. In this article, we will explore the conceptual overview of React Native’s internals and its new architecture. We will discuss the rendering process, build tools, the Fabric rendering system, the renderer’s phases, cross-platform implementation, view flattening, threading model, and the use of Hermes as a JavaScript engine. Let’s dive in! React Native Architecture

we can divide Architecture into two sections :

1. Rendering

The rendering process in React Native is crucial for creating and updating the user interface. It involves transforming React components into platform-specific views. Here are the key aspects of React Native’s rendering. Rendering can be divided into five parts

  • Fabric
  • Render, commit, and mount
  • Cross Platform implementation
  • View Flattering
  • Threading Model

2. Build Tools

React Native provides various build tools to enhance the development process. One of these tools is Hermes, an open-source JavaScript engine optimized for React Native. Build tools can be divided into one part.

  • Bundled Hermes

let's go to the deep drive in each part

Fabric

Fabric is React Native’s new rendering system. Its main purpose is to unify the rendering logic in C++ for improved performance and efficiency. Fabric allows React Native to execute the same React framework code as React for the web but renders it to general platform views (host views ) instead of DOM nodes. The Fabric Renderer exists in JavaScript and targets interfaces made available by C++ code

Render, Commit, and Mount

The React Native renderer goes through a sequence of work to render React logic to a host platform. This sequence of work is called the render pipeline and occurs for initial renders and updates to the UI state.

The rendering process in React Native can be broken down into three phases: render, commit, and mount.

  1. Render: During the render phase, React executes the product logic and creates React Element Trees in JavaScript. The renderer then React Shadow Tree Tree in C++ for each host component.
  2. Commit: After the React Shadow Tree is fully created, a commit is triggered. This promotes the React Element Tree and the React Shadow Tree as the “next tree” to be mounted. Layout calculation is also scheduled during this phase.
  3. Mount: The mount phase transforms the React Shadow Tree, along with its layout information, into a Host View Tree, which represents the rendered pixels on the screen. This phase configures each host view using props from the corresponding React Shadow Node.

let's take an example -

function MyComponent () {
return (
<View>
<Text>my title</Text> </View>
)
}
// <MyComponent />

<MyComponent /> is a React Element. React recursively reduces this React Element to a terminal React Host Component by invoking it until every React Element cannot be reduced any further.

Phase 1. Render

During this process of element reduction, as each React Element is invoked, the renderer also synchronously creates a React Shadow Node. This happens only for React Host Components, not for React Composite Components.

<View> -> ViewShadowNode object

<Text>-> TextShadowNode object

Whenever React creates a parent-child relationship between two React Element Nodes. The renderer creates the same relationship between the corresponding React Shadow Nodes. These are synchronous and thread-safe operations that are executed from React (JavaScript) into the renderer (C++), usually on the JavaScript thread.

The React Element Tree (and its constituent React Element Nodes) does not exist indefinitely.

It is a temporal representation materialized by “fibers” in React. Each “fiber” that represents a host component stores a C++ pointer to the React Shadow Node, made possible by JSI.

The React Shadow Tree is immutable. In order to update any React Shadow Node, the renderer creates a new React Shadow Tree

Phase 2. Commit

The commit phase consists of two operations: Layout Calculation and Tree Promotion.

1: Layout Calculation:

This operation calculates the position and size of each React Shadow Node. This involves invoking Yoga to calculate the layout of each React Shadow Node. The actual calculation requires each React Shadow Node’s styles which originate from a React Element in JavaScript.It also requires the layout constraints of the root of the React Shadow Tree, which determines the amount of available space that the resulting nodes can occupy.

2: Tree Promotion (New Tree → Next Tree):

This operation promotes the new React Shadow Tree as the “next tree” to be mounted. This promotion indicates that the new React Shadow Tree has all the information to be mounted and represents the latest state of the React Element Tree. The “next tree” mounts on the next “tick” of the UI Thread.

Phase 3. Mount

The mount phase transforms the React Shadow Tree into a Host View Tree with rendered pixels on the screen. Each host view is then configured to use props from its React Shadow Node. size and position are configured using the calculated layout information.

The operations are synchronously executed on the UI thread. If the commit phase executes on the background thread, the mounting phase is scheduled for the next “tick” of the UI thread. On the other hand, if the commit phase executes on the UI thread, the mounting phase executes synchronously on the same thread.

  • Scheduling, implementation, and execution of the mounting phase heavily depend on the host platform.
  • During the initial render, the “previously rendered tree” is empty. As such, the tree diffing step will result in a list of mutation operations that consists only of creating views, setting props, and adding views to each other. Tree diffing becomes more important for performance when processing React State Updates.
  • In current production tests, a React Shadow Tree typically consists of about 600–1000 React Shadow Nodes (before view flattening), the trees get reduced to ~200 nodes after view flattening.

The mounting phase consists of these three steps:

  • Tree Diffing: This step computes the diff between the “previously rendered tree” and the “next tree” entirely in C++. The result is a list of atomic mutation operations to be performed on host views (e.g. createView, updateView, removeView, deleteView, etc). Avoid creating unnecessary host views.
  • Tree Promotion (Next Tree → Rendered Tree): This step atomically promotes the “next tree” to the “previously rendered tree” so that the next mount phase computes a diff against the proper tree.
  • View Mounting: This step applies the atomic mutation operations to corresponding host views. This step executes in the host platform on the UI thread.

View Flattening

View Flattening is an optimization technique employed by the React Native renderer to avoid deep layout trees and improve performance. Here’s how it works:

React Element Nodes that only affect the layout of a view without rendering anything on the screen are called “Layout-Only” Nodes. Conceptually, each of the Nodes of the React Element Tree has a 1:1 relationship with a view on the screen. These nodes, such as views with margin, padding, background color, or opacity, can lead to poor performance when rendering deep React Element Trees.

Source - react-native office
Source - React native official document

Note that Views (2) and (3) are “Layout Only” views because they are rendered on the screen but they only render a margin of 10 px on top of their children.

To address this, the renderer implements a View Flattening mechanism that merges or flattens these “Layout-Only” Nodes. This reduces the depth of the host view hierarchy rendered on the screen. The View Flattening algorithm is integrated into the renderer’s diffing stage, which means that we don’t use extra CPU cycles to optimize the React Element Tree flattening of these types of views utilizing C++ for optimal performance. As a result, the React Element Tree is optimized and rendered more efficiently.

Source - React-native official documents

the Views (2) and (3) would be flattened as part of the “diffing algorithm” and as a result, their styles will be merged into the View (1):

Cross-Platform Implementation

In the previous render system of React Native, the React Shadow Tree, layout logic, and View Flattening algorithm were implemented once for each platform.

The current renderer was designed to be a cross-platform solution by sharing a core C++ implementation. The memory footprint of each React Shadow Node is smaller in C++ than it would be if allocated from Kotlin or Swift. It is important to recognize that the renderer use case for Android still incurs the cost of JNI for two primary use cases:

  • Layout calculation of complex views (e.g. Text, TextInput, etc.) requires sending props over JNI.
  • The mount phase requires sending mutation operations over JNI.

The renderer provides two sides of its C++ APIs:

  1. To communicate with React: React communicates with the renderer to render a React Tree and to “listen” for events (e.g. onLayout, onKeyPress, touch, etc).
  2. To communicate with the host platform: the React Native renderer communicates with the host platform to mount host views on the screen (create, insert, update, or delete host views) and it listens for events that are generated by the user on the host platform.

Threading Model

React Native utilizes three different threads for its operations:

  1. UI Thread (Main Thread): The UI thread is responsible for manipulating host views and handling user interactions.
  2. JavaScript Thread: The JavaScript thread executes React’s render phase and manages the application’s JavaScript logic.
  3. Background Thread: The background thread is dedicated to layout calculations, improving performance by offloading intensive computations from the UI thread.

This threading model allows React Native to provide thread-safe and synchronous APIs while maintaining efficient communication between threads.

Benefits of the New Renderer System

The new renderer system brings several benefits to React Native:

  1. Enhanced User Experience: The new system resolves the “jump” issue that occurred with the asynchronous layout in the legacy architecture. It provides multi-priority and synchronous events, ensuring the timely handling of user interactions.
  2. Integration with React Suspense: The new renderer system seamlessly integrates with React Suspense, enabling the intuitive design of data fetching in React apps.
  3. React Concurrent Features: It enables the use of React Concurrent Features in React Native, allowing concurrent rendering and better performance.
  4. Server-Side Rendering: The new architecture makes it easier to implement server-side rendering for React Native applications.

Code Quality, Performance, and Extensibility

Apart from the user experience improvements, the new architecture offers benefits in code quality, performance, and extensibility:

  1. Type Safety: The new renderer system ensures type safety, reducing the chances of runtime errors.
  2. Shared C++ Core: By sharing a core C++ implementation, the new architecture minimizes memory footprint and improves performance.
  3. Better Host Platform Interoperability: The new architecture improves interoperability with host platforms, such as Android and iOS.
  4. Faster Startup: React Native apps built with the new architecture experience faster startup times.
  5. Reduced Serialization of Data: The new architecture reduces the serialization of data between JavaScript and the host platform through JavaScript Interfaces (JSI).

Bundled Hermes

Starting from React Native 0.69.0, Hermes is used by default as the JavaScript engine. It offers improved start-up time, decreased memory usage, and a smaller app size compared to JavaScriptCore. With Bundled Hermes, every React Native version is built alongside a specific Hermes version, eliminating the need for additional configuration.

Why a New React Native Architecture?

The previous React Native architecture had certain drawbacks, such as heavy reliance on the bridge for inter-communication between threads, asynchronous messaging, slow data transmission, and unexpected page jumps during UI updates.

The new React Native architecture addresses these issues by introducing the Fabric rendering system, shared C++ core, view flattening, and other optimizations. These enhancements result in improved performance, code quality, and user experience. Additionally, the adoption of Hermes as the default JavaScript engine further enhances start-up time, memory usage, and app size.

By embracing this new architecture, React Native continues to evolve, offering developers a more efficient and powerful framework for building high-quality cross-platform mobile applications.

That concludes our conceptual overview of React Native’s architecture. We hope this article provides you with valuable insights into how React Native works internally and the benefits of its new rendering system.

Happy coding!

--

--