Understanding React - Part 2.1. Breaking Down the React Work LoopAugust 24# Tech# Front-End# React
Introduction
In the previous post, we covered the basic concepts of React, including how React describes the UI and introduced the concept of Fiber. Fiber can be thought of as the building blocks of React applications, much like bricks in a skyscraper. But how does React efficiently manage and operate on these "bricks" to construct the applications users interact with? In this post, we'll break down the React Work Loop, a core function of React, to understand how it renders applications efficiently using the Fiber architecture.
The Fiber Tree
In web development, we use the DOM tree to represent and manage the structure of a document. The tree structure is well-suited to handling hierarchies, making it an excellent choice for representing complex relationships, such as parent-child elements in a document.
Similarly, React uses a tree structure to manage Fiber nodes. This Fiber tree allows React to traverse nodes efficiently, collect updates, and apply patches as necessary. The tree structure also makes it easier to manage re-renders and efficiently update the UI when changes occur.
If you've worked with React, you've likely written code that looks something like this:
Whether before or after React 18, the purpose of this code remains the same: to render the root of the application (usually the <App />
component) inside an HTML element (commonly the div#root
). Everything we define in React will eventually appear under this div#root
element.
When you execute this code, several operations happen behind the scenes:
- React creates a special Fiber node with the tag
HostRoot
. This is the root of the Fiber tree and the starting point for the work loop. - React creates a FiberRoot, which contains a reference to both the actual DOM container (
div#root
) and the root of the Fiber tree (theHostRoot
). - The work loop begins.
While the FiberRoot and Fiber nodes work together, they are not the same structure. Let's take a closer look at the FiberRoot.
The FiberRoot
The FiberRoot is a specialized data structure in React that acts as a connection point between the Fiber tree and the host environment (e.g., the DOM). It also plays a critical role in managing the scheduling of updates. Here's an example of what the FiberRoot structure looks like:
The FiberRoot serves as a bridge between the Fiber tree and the actual DOM or host environment. When React creates an alternate Fiber tree based on UI updates, it can swap out the entire tree by assigning the alternate of the HostRoot
to the current
property of the FiberRoot. This ensures that the Fiber tree is always synchronized with the UI.
Additionally, the FiberRoot manages "lanes," a concept that allows React to optimize performance by organizing and prioritizing updates. We'll discuss lanes in more detail in a later post.
The Alternate Architecture
React operates with two Fiber trees: the current tree and the work-in-progress tree. The current tree represents the UI currently being displayed on the screen. When an update occurs (such as a state change triggered by a user action), React begins to traverse the current Fiber tree. For each Fiber node that needs to be updated, React creates a new work-in-progress Fiber node and connects it to the current node via the alternate
property.
As React traverses the current tree and generates the work-in-progress tree, it applies updates and prepares the UI for rendering. Once the updates are complete, React swaps the two trees, promoting the work-in-progress tree to the current tree.
This alternate architecture is highly effective because it allows React to prepare updates offscreen before applying them to the DOM. This approach optimizes performance by minimizing unnecessary renders and ensuring that updates are applied atomically, leading to a smoother user experience.
The Work Loop
Now that we've explored the structure of the Fiber tree, let's dive into the internal workings of the React Work Loop. The Work Loop is the heart of React's rendering process. It is responsible for scheduling updates, traversing the Fiber tree, and committing changes to the host environment (like the DOM).
The Work Loop consists of three main stages:
- Scheduling Stage: React schedules updates based on their priority. This ensures that higher-priority updates (such as user interactions) are handled promptly, while lower-priority updates (like background data fetching) can be deferred.
- Render Stage: During this stage, React performs two key operations:
beginWork
andcompleteWork
. It traverses the Fiber tree, creating, updating, or deleting Fiber nodes as necessary. It also collects side effects and prepares updates for the host environment. - Commit Stage: In the final stage, React commits updates to the host environment (e.g., the DOM) and applies any side effects, such as running
useEffect
hooks.
The diagram below illustrates the workflow:
- Create Root: A
FiberRoot
andHostRoot
are created, linking the DOM to the Fiber tree. - Render
<App />
: An update is created, and the work loop begins. - Work Loop Execution: The work-in-progress Fiber tree is generated, and updates are applied to the UI.
- Tree Swap: The current Fiber tree is swapped with the work-in-progress tree, keeping the UI in sync with the latest state.
Triggering the Work Loop
Previously, we examined the structure of the Work Loop and its role in React’s rendering process. The Work Loop is initially triggered when we create a root and call the render function to display the app. However, the most frequent way to trigger the Work Loop is through state updates, such as calling setState when using useState hooks. We’ll explore hooks in more detail later, but for now, let’s focus on how the Work Loop is triggered by state changes.
The core of this process is the scheduleUpdateOnFiber function, which acts as the entry point for the Work Loop. It’s important to note that scheduleUpdateOnFiber doesn’t simply trigger the Work Loop synchronously every time it’s called. While this would be straightforward, it can introduce performance issues. For example, calling setState multiple times in quick succession would result in multiple Work Loops, causing React to traverse the Fiber tree repeatedly and potentially blocking the main thread. This is inefficient, especially in complex applications with numerous state updates.
Evolution of the Work Loop
React has evolved to address performance challenges in managing frequent state updates. For instance, imagine you have a form with several input fields, and each change triggers a state update. If each setState call resulted in an immediate Work Loop traversal, React would have to process the entire Fiber tree multiple times, even if the updates are minor or frequent. This would degrade performance, especially for complex UIs or apps with numerous components.
In earlier versions of React, updates were handled asynchronously. When you triggered a state change, React wouldn’t immediately begin processing the Work Loop. Instead, it would schedule the update in the JavaScript microtask queue, allowing the main thread to complete any ongoing tasks. Once the main thread became idle, the queued updates would be processed. This approach prevented the main thread from being blocked by frequent updates, improving the user experience in scenarios like real-time input validation or dynamic search suggestions.
Here’s an example to illustrate:
In a form like this, every keystroke triggers setState
. Without batching or scheduling, each keystroke would result in a complete tree traversal, making the app feel sluggish. Asynchronous updates alleviated this issue, but there was still room for improvement, especially when multiple state changes occurred at once.
To optimize further, React introduced the lane model. Every update is assigned a priority (a lane), and updates with the same priority are batched together. For example, multiple input changes in a form might be handled within the same Work Loop, reducing the need to reprocess the tree after every single change. This batching of updates improves efficiency and responsiveness.
However, there were still cases where even batched updates could block the user interface, especially when complex calculations or numerous updates needed to be processed. React addressed this by introducing Concurrent Mode, which allows React to pause and resume rendering. For example, when typing in a large form or dragging elements in a complex UI, React can temporarily pause less critical updates, prioritizing the user’s interaction to ensure a responsive interface.
These examples give us insight into the scheduling stage of the Work Loop, where React decides when and how to process updates. We'll explore this scheduling mechanism in more detail in a future post, covering the algorithms and strategies that make React’s update management efficient and responsive.
In the End
The React Work Loop is a vital component of how React efficiently manages updates and renders applications, ensuring optimal performance and responsiveness. Over the years, React has evolved its Work Loop with features like asynchronous rendering, the lane model, and Concurrent Mode, all designed to tackle the challenges of managing state and UI updates.
In the next post, we’ll focus on the Render and Commit phases of the Work Loop, as these two stages are central to how React handles state updates and re-renders the UI. The Render phase processes changes to the Fiber tree, while the Commit phase ensures that these changes are synchronized with the DOM. These are the core tasks that directly impact how the UI responds to user interactions.
The Schedule stage, on the other hand, operates at a higher level. It’s responsible for managing and prioritizing the update processes across different components and states. However, to fully understand the significance of the Schedule stage, we first need to grasp how the internal rendering and committing of updates work. Once we dive into these inner workings, we’ll be better equipped to explore the high-level scheduling strategies React employs.