This article is reprinted from what is probably the most popular way to open React Fiber.

Why Fiber architecture

Why did React introduce Fiber architecture? Take a look at the fire chart below. This is a list of render resource consumption below React V15. The entire rendering cost 130ms. 🔴 uses React to recursively compare the VirtualDOM tree, find nodes that need to be changed, and update them synchronously. The React process is called Reconcilation.

React appropriates browser resources during Reconcilation, leading to a lack of response for user-triggered events and a drop of frames that users can sense.

React in sync mode:

React in optimized Concurrent mode:

The React Reconcilation is A CPU-intensive operation, which is the equivalent of what we called a ‘long process’ above. Therefore, the original intention is the same as process scheduling. We should give priority to high-priority processes or short processes and not allow long processes to occupy resources for a long time.

So how is React optimized? 🔴 In order to give users the ‘illusion’ that applications are fast, we can’t let an application hog resources for long. You can think of browser rendering, layout, drawing, resource loading (e.g., HTML parsing), event response, and script execution as operating system ‘processes’ that require scheduling strategies to properly allocate CPU resources in order to increase browser user responsiveness while maintaining task execution efficiency.

🔴 So React makes its Reconcilation process into a Reconcilation that can be interrupted through the Fiber framework. In addition to allowing the browser to respond to user interactions in a timely manner, ceding execution to the CPU has other benefits:

  • Batch delay operation on the DOM provides a better user experience than operating on a large number of DOM nodes at once.

That’s why React needs Fiber.

What is the Fiber

For React, Fiber can be understood from two perspectives:

A process control language

Fiber is also called a coroutine, or Fiber.

** Coroutines are not the same as threads in that they have no concurrency or parallelism (they need to work with threads). ** To understand coroutines, you have to look at them in terms of ordinary functions, using Generator as an example:

Normal functions cannot be interrupted or resumed during execution:

const tasks = []
function run() {
  let task
  while (task = tasks.shift()) {
    execute(task)
  }
}
Copy the code

The Generator can:

Const tasks = [] function * run() {let task while (task = tasks.shift()) {// 🔴 If (hasHighPriorityEvent()) {yield} if (hasHighPriorityEvent()) {yield} execute(task) } }Copy the code

The React Fiber idea is consistent with the concept of coroutines: 🔴 React rendering can be interrupted, and control can be handed back to the browser for higher-priority tasks, which can be resumed when the browser is idle.

Now you should have the following questions:

1️ browser has no preemption condition, so React can only use the surrender mechanism?

2️ how to determine that there is a high priority task to be dealt with, that is, when to surrender?

3️ discount Why not use Generator?

1️ retail: Yes, React voluntarily surrender.

One is that there is no process-like concept in the browser, the boundaries between tasks are blurred and there is no context, so there is no condition for interruption/recovery. Second, there is no preemption mechanism, so we cannot interrupt an executing program.

So we have to use a control transfer mechanism like coroutine. This is different from the process Scheduling strategy mentioned above. It has a more technical name: Cooperative Scheduling, and its counterpart is Preemptive Scheduling.

This is a “contract” scheduling that requires our programs and browsers to be tightly bound and trust each other. For example, the browser can assign us a slice of execution time (via requestIdleCallback, described below), and we need to execute within this time by convention, giving control back to the browser.

Answer 2 ️ ⃣ : requestIdleCallback API

HasHighPriorityEvent () in the code example above is not implemented in current browsers and there is no way to tell if there is a higher priority task waiting to be executed.

Another way to think about it is to give up control through the mechanism of timeout checking. The solution is to determine a reasonable running time, then detect timeout at an appropriate checkpoint (for example, every time a small task is executed), stop execution if it times out, and transfer control to the browser.

For example, in order for the view to run smoothly, time slices can be divided according to the minimum human perception of 60 frames per second, so that each time slice is 16ms.

The requestIdleCallback API is provided by the browser:

window.requestIdleCallback( callback: (dealine: IdleDeadline) => void, option? : {timeout: number} )Copy the code

The interfaces of IdleDeadline are as follows:

Interface IdleDealine {didTimeout: Boolean timeRemaining(): DOMHighResTimeStamp // Remaining time of the task}Copy the code

As the name implies, requestIdleCallback means to let the browser execute our callback as soon as it’s ‘available.’ This callback is passed in a deadline indicating how much time the browser has available for us to execute, and we’d better finish within this time frame in order to avoid delay.

When will the browser be free?

Let’s start by looking at what the browser might do within a Frame (think of it as a loop of an event loop) :

The browser may perform the following tasks in a frame, and the order in which they are executed is generally fixed:

  • Handle user input events
  • Javascript execution
  • RequestAnimation call
  • Layout of the Layout
  • Draw the Paint

It says that the ideal frame time is 16ms (1000ms / 60), and if the browser has done the above tasks (after layout and drawing) and still has time to spare, the browser will call the requestIdleCallback callback. Such as:

However, when the browser is busy, there may be no surplus time and the requestIdleCallback callback may not be executed. To avoid starvation, you can specify a timeout with the second parameter of the requestIdleCallback.

DOM manipulation in the requestIdleCallback is also not recommended, as this may result in style recalculation or rearrangement (such as calling getBoundingClientRect immediately after DOM manipulation), which is difficult to predict and may cause the callback to time out and drop frames.

RequestIdleCallback is currently only supported by Chrome. So React currently implements one of its own. It uses MessageChannel emulation to defer the callback until after the ‘draw operation’.

Task priority

As mentioned above, you can set a timeout to avoid starving tasks. This timeout is not dead, low-priority tasks can wait, and high-priority tasks should be executed first. React currently has five predefined priorities, which I explained in React-Events:

  • Immediate(-1)– Tasks of this priority are executed synchronously, or immediately without interruption
  • UserBlocking(250ms)– These tasks are usually the result of user interaction and require immediate feedback
  • Normal (5s)– Respond to tasks that don’t need to be felt immediately, such as network requests
  • Low (10s)– These tasks can be postponed, but should be executed eventually. For example, analysis notifications
  • Idle (no timeout)– you may starve to death for unnecessary tasks (e.g., hidden content)

A3 ️ discount: Too much trouble

  • Generator cannot be relinquished in the middle of the stack. For example, if you want to delegate between nested function calls, you first need to wrap these functions as generators, and the delegate in the middle of the stack is cumbersome and difficult to understand. In addition to the syntax overhead, existing generator implementations are expensive, so you might as well not use them.

  • The Generator is stateful and it is difficult to recover these states in the middle.

An execution unit

Another way to read Fiber is “Fiber” : it’s a data structure or execution unit. Regardless of what the data structure looks like, 🔴 treats it as an execution unit. After each execution, React checks how much time is left and cedes control if it doesn’t.

React does not use a language/syntactic transfer mechanism such as Generator, but implements its own dispatch transfer mechanism. This mechanism is based on the ‘Fiber’ execution unit, and it works as follows:

Assuming the user calls the setState update component, the task to be updated is queued and then requested by the browser for scheduling via requestIdleCallback:

updateQueue.push(updateTask);
requestIdleCallback(performWork, {timeout});
Copy the code

Now the browser is idle or timed out and calls performWork to perform the task:

// 1️ performWork will get a Deadline, Function performWork(deadline) {// 2️ discount while (updateQueue. Length > 0 && deadline.timeRemaining() > ENOUGH_TIME) { workLoop(deadline); // 3️ discount if (updatequue. Length > 0) {requestIdleCallback(performWork); }}Copy the code

The workLoop will execute the update task from the updateQueue. After each “execution unit” is completed, it checks to see if there is enough time left. If there is enough time left, it will execute the next execution unit, if not, it will stop execution, save the scene, and wait for the next execution to resume.

/ / save the current processing site let nextUnitOfWork: Fiber | undefined / / save a unit of work need to handle the let topWork: Fiber | undefined / / save the first unit of work function workLoop (deadline: IdleDeadline) {if (nextUnitOfWork == null) {nextUnitOfWork = topWork = getNextUnitOfWork(); } // 🔴 check the remaining time after each execution unit // If interrupted, The next execution starts with nextUnitOfWork while (nextUnitOfWork && deadline. TimeRemaining () > ENOUGH_TIME) {// PerformUnitOfWork nextUnitOfWork = performUnitOfWork(nextUnitOfWork, topWork); If (pendingCommit) {commitAllWork(pendingCommit); }}Copy the code

React Fiber retrofit

1. Data structure adjustment

On the left is the Virtual DOM, and on the right can be seen as diff’s recursive call stack.

Before the React 16 Reconcilation was mentioned above, it was implemented synchronously and recursively. That is, it is a Reconcilation algorithm based on the function ‘call Stack’, which is why it is often called Stack Reconcilation.

The stack is nice, the code is small, the recursion is easy to understand, at least better than the React Fiber architecture, and recursion is great for dealing with nested data structures like trees.

However, this stack-dependent approach cannot be broken at will and is difficult to recover, which is not conducive to asynchronous processing. This call stack is out of your program’s control, and if you want to restore the recursive scene, you may need to start from scratch and revert to the previous call stack.

Therefore, first of all, we need to adjust the existing data structure of React, simulate the function call stack, decompose the things that need to be handled recursively into incremental execution units, and convert recursion into iteration.

React currently uses linked lists. Each VirtualDOM node is represented internally by Fiber, which looks like this:

Export type Fiber = {// Fiber type information type: any, //... / / ⚛ ️ chain table structure / / points to the parent node, the node or render the component return: Fiber | null, / / points to the first child child: Fiber | null, brother / / points to the next node (: Fiber | null, }Copy the code

It’s easier to visualize the relationship with a picture:

The use of the linked list structure was an end result, not an end, and the React developers’ initial goal was to emulate the call stack.

The call stack is most often used to hold the return address of a subroutine. When any subroutine is called, the main program must temporarily store the address to which the subroutine should return when it finishes running. Therefore, if the called subroutine also calls other subroutines, its own return address must be placed on the call stack and retrieved after it has finished running. In addition to the return address, local variables, function parameters, and environment passes are saved.

React Fiber is also known as a Virtual Stack Frame, and you can think of it as a function call Stack. The structure is very similar:

Function call stack Fiber
The basic unit function Virtual DOM node
The input Function parameters Props
The local state The local variable State
The output Function return value React Element
At a lower level Nested function calls Child nodes (the child)
The superior reference The return address The parent node (return)

Fiber, like the call stack frame, holds the context information that the node processes, which is more controllable because it is implemented manually and can be kept in memory for interruption and recovery at any time.

With this data structure adjustment, you can now work with these nodes in an iterative manner. Take a look at the implementation of performUnitOfWork, which is essentially a depth-first traversal:

/** * @params topWork */ function performUnitOfWork(fiber: fiber, topWork: Fiber) {// beginWork(Fiber); If (fiber.child) {return fiber.child; // If (fiber.child) {return fiber.child; } // Let temp = fiber; while (temp) { completeWork(temp); If (temp === topWork) {break} if (temp === topWork) {return temp.sibling; } return temp = temp. Return; }}Copy the code

Fiber is the unit of work. PerformUnitOfWork is responsible for operating on Fiber and returning to the next Fiber in the order of depth traversal.

Because of the linked list structure, even if the process is interrupted, we can always continue to iterate over the Fiber that was left unprocessed last time.

The entire iteration sequence is the same as the previous iteration recursively, assuming an update in div.app:

For example, if you break at text(hello), the next processing will start at the P node.

Another benefit of this data structure adjustment is that when certain nodes fail, we can print out the entire “stack of nodes” by simply tracing the node’s return.

2. Split the two phases

In addition to the Fiber unit of work split, the two-phase split was also a very important transformation that had been submitted while Diff was going on. Let’s look at the differences:

  • ⚛️ Coordination phase: This phase can be thought of as the Diff phase, which can be interrupted. This phase finds out all node changes, such as node additions, deletions, property changes, etc. React calls these changes’ side effects’. The following lifecycle hooks are called during the coordination phase:

    • constructor
    • ComponentWillMount abandoned
    • ComponentWillReceiveProps abandoned
    • static getDerivedStateFromProps
    • shouldComponentUpdate
    • ComponentWillUpdate abandoned
    • render
  • ⚛️ Commit phase: The Effects calculated in the previous phase that need to be addressed are executed at once. This phase must be executed synchronously and without interruption. These lifecycle hooks are executed during the commit phase:

    • GetSnapshotBeforeUpdate () Strictly speaking, this is called before the commit phase
    • componentDidMount
    • componentDidUpdate
    • componentWillUnmount

In other words, React cedes control if the time slice runs out during the coordination phase. Because the work performed in the coordination phase does not result in any user-visible changes, ceding control during this phase is not a problem.

Note that the React coordination lifecycle hook may be called multiple times because the coordination phase can be interrupted, resumed, or even redone!! For example, componentWillMount might be called twice.

Therefore, it is recommended that the coordination phase lifecycle hooks do not contain side effects. React scrapped life cycle methods that might have side effects, such as componentWillMount and componentWillUpdate. We won’t be able to use them after V17, so existing applications should migrate as soon as possible.

Now you know why the commit phase must be executed synchronously and without interruption? Because we need to properly handle all kinds of side effects, including DOM changes, asynchronous requests you make in componentDidMount, side effects defined in useEffect… Because of the side effects, you must ensure that the calls are made only once in sequence, and that there are changes that the user can detect and cannot go wrong.

3. Reconcilation

Then comes the Reconcilation phase, which is known as the Diff and Reconcilation(for the sake of understanding, this text does not distinguish between the two, which are the same thing). The idea is not much different than before Fiber refactoring, except that there is no recursive alignment and no immediate commit of changes.

Let’s take a closer look at the structure of Fiber:

Interface Fiber {/** * ⚛️ Node type information */ // / label the Fiber type, such as function component, class component, host component tag: WorkTag, / / the node element type, it is a specific type of component, function module, host components (string) type: any, / * * * ⚛ ️ structure information * / return: Fiber | null, the child: Fiber | null, (: Fiber | null, / / the child nodes of the only key, namely we render a list of the incoming key key attributes: Null | string, / * * * ⚛ ️ node state of * / / / instance (state) : / / components for host, here to save the host instance of a component, such as the DOM node. // for the function component, this is empty because the function component has no instance stateNode: any, // new, pendingProps pendingProps: MemoizedProps: any, // The props used to create The output. // memoizedState: Any, /** * ⚛️ Side effects */ // Side effects of the current node, such as node update, delete, move effectTag: SideEffectTag, / / and the relationship between nodes, the React also use the list to connect all have side effects of Fiber nextEffect: Fiber | null, / * * * ⚛ ️ double * * point to in the old tree node/alternate: Fiber | null, }Copy the code

Fiber contains properties that can be broken down into five parts:

  • 🆕 Structure information – We’ve already seen this before, Fiber uses a linked list to represent node positions in a tree.

  • Node type information – this is also easy to understand, with tag representing the node’s classification and type holding specific type values such as div and MyComp.

  • Node state – The component instances, props, state, and so on of the node, which affect the output of the component.

  • 🆕 Side effects – This is new, too. The “side effects” (change requirements) found during Reconciliation are stored in the node’s effectTag (think of it as marking a tag). So how do you collect all the node side effects from this render? A linked list structure is also used here. React will connect all nodes with “side effects” through nextEffect during traverse.

  • 🆕 stunt-React Builds a new tree (officially called the workInProgress tree, WIP tree) during the Reconciliation process, which can be considered as a tree representing the current work progress. React builds a WIP tree while comparing it to an old rendered tree. Alternate refers to the equivalent node of an old tree.

Now you can zoom in and see how beginWork compares Fiber:

function beginWork(fiber: Fiber): Fiber | undefined {the if (Fiber. The tag = = = WorkTag. HostComponent) {/ / host node diff diffHostComponent (Fiber)} else if (Fiber. The tag // Diff diffClassComponent(fiber)} else if (fiber. Tag === = WorkTag. FunctionComponent) {/ / function component node diff diffFunctionalComponent (fiber)} else {/ /... Other types of nodes, omit}}Copy the code

Host node comparison:

function diffHostComponent(fiber: If (fiber.statenode == null) {fiber.statenode = createHostComponent(Fiber)} else { updateHostComponent(fiber) } const newChildren = fiber.pendingProps.children; // diffChildren(fiber, newChildren); }Copy the code

Class component node comparison is similar:

function diffClassComponent(fiber: If (fiber.statenode == null) {fiber.statenode = createInstance(Fiber); } if (fiber.hasMounted) {applybeforeUpdateHooks(fiber)} else {// Call premount lifecycle hooks ApplybeforeMountHooks (fiber)} // Render new node const newChildren = fiber.statenode.render (); // diffChildren(fiber, newChildren); fiber.memoizedState = fiber.stateNode.state }Copy the code

Child node comparison:

function diffChildren(fiber: Fiber, newChildren: React.ReactNode) { let oldFiber = fiber.alternate ? fiber.alternate.child : null; If (oldFiber == null) {mountChildFibers(fiber, newChildren) return} let index = 0; if (oldFiber == null) {mountChildFibers(fiber, newChildren) return} let index = 0; let newFiber = null; / / the new child node const elements = extraElements (newChildren) / / than children elements while (index < elements. The length | | oldFiber! = null) { const prevFiber = newFiber; const element = elements[index] const sameType = isSameType(element, oldFiber) if (sameType) { newFiber = cloneFiber(oldFiber, Element) // newFiber. Alternate = oldFiber // Add newFiber. EffectTag = UPDATE newFiber (element && ! SameType) {newFiber = createFiber(Element) newFiber. EffectTag = PLACEMENT newFiber. Return = fiber (oldFiber && ! sameType) { oldFiber.effectTag = DELETION; oldFiber.nextEffect = fiber.nextEffect fiber.nextEffect = oldFiber } if (oldFiber) { oldFiber = oldFiber.sibling; } if (index == 0) { fiber.child = newFiber; } else if (prevFiber && element) { prevFiber.sibling = newFiber; } index++ } }Copy the code

The code above is a rough approximation of the Reconciliation process, but it’s enough to understand the basics of React.

The picture above shows the Reconciliation state with the old tree on the left and the WIP tree on the right. The nodes that need to be changed are labeled. During the commit phase, React applies the changes to the labeled nodes.

4. Double buffer

WIP tree construction is similar to “Double Buffering” in graphics. Graphics engines usually use Double Buffering to draw images to a buffer and then pass them to the screen once for display. This prevents screen shaking and optimizes rendering performance.

WIP tree construction is a technique similar to ‘Double Buffering’ in graphics. Graphics engines use Double Buffering to draw images into a buffer and then pass them to the screen at once to prevent screen shaking and optimize rendering performance.

Another important aspect of dual-cache technology is exception handling. For example, when a node throws an exception, it can still use the node of the old tree to avoid the whole tree hanging.

You can think of a WIP tree as a branch of functionality that forks out of an old tree, where you can add or remove features without affecting the old branch if it goes wrong. When your branch is tested and refined, merge it into the old branch and replace it. Perhaps this is where the term ‘commit phase’ comes from?

5. Collection and submission of side effects

The next step is to concatenate all the nodes marked with Effect. This can be done in the completeWork, for example:

The function completeWork (fiber) {const parent = fiber. The return / / reach the top if the parent (= = null | | fiber = = = topWork) { pendingCommit = fiber return } if (fiber.effectTag ! = null) { if (parent.nextEffect) { parent.nextEffect.nextEffect = fiber } else { parent.nextEffect = fiber } } else if (fiber.nextEffect) { parent.nextEffect = fiber.nextEffect } }Copy the code

Finally, submit all side effects:

Function commitAllWork(fiber) {let next = fiber while(next) {if (fiber. EffectTag) { CommitWork (fiber)} next = fiber. NextEffect} pendingCommit = nextUnitOfWork = topWork = nullCopy the code

With Concurrent Mode enabled, we get the following benefits (see Concurrent Rendering in React):

  • Quickly respond to user operations and input to improve user interaction experience.
  • Make the animation smoother, and by scheduling, keep the frame rate high.
  • Make good use ofI/OOperate the idle period or CPU idle period and do some pre-rendering. React can wait until the CPU is idle to render content that is not visible offscreen and has the lowest priority. This is similar to preloading technologies such as preload in browsers.
  • withSuspenseThe priority of the load state is reduced and the splash screen is reduced. For example, when data is returned quickly, the loading status can be directly displayed instead of being displayed, avoiding flash screen. Load state explicitly only if there is no return from timeout.

Refer to the article

This is probably the most popular way to open React Fiber

React events and the future

Learn the React component and hooks fundamentals from Preact

React Fiber