1. Why Fiber

Those who have used React15 probably know in general that version 15 is based on Stack Reconcilation. It’s a recursive, synchronous approach. The advantage of stacks is that you can implement diff functionality with very little code. And it’s very easy to understand. But it also introduces serious performance problems. Here’s why.

1.1 Partial threads in browser rendering



  • GUI rendering thread: responsible for rendering browser interfaces, parsing HTML, CSS, building DOM trees and RenderObject trees, layout and drawing, etc.
  • JS engine thread: responsible for processing Javascript script programs. The GUI rendering thread and the JS engine thread are mutually exclusive, so if the JS execution time is too long, the page rendering will be incoherent and the page rendering load will block.
  • Event-triggered threads: Belong to the browser, not the JS engine, and are used to control Event loops.
  • Timing trigger thread: the legendary thread where setInterval and setTimeout are located, uses a separate thread to time and trigger the timing (after the timing is finished, it is added to the event queue and waits for the JS engine to be idle).
  • Asynchronous HTTP request thread: When a state change is detected, if a callback function is set, the asynchronous thread generates a state change event and places the callback in the event queue. It’s executed by the JavaScript engine.

As you can see, there are many, many threads in the browser, but JS is single-threaded, and if one of them blocks, the other threads will not execute for a long time. Therefore, threads are mutually exclusive. If JS were multithreaded, what would happen? When thread A modifies Dom1 data and thread 2 deletes Dom1 data, the browser will be conflicted. Therefore, JS must be single-threaded.

1.2 Why to Optimize Stack Reconcilation

The Stack Reconcilation blocks the entire thread when the Reconcilation is calculated, and the entire rendering process must be completed continuously. Other tasks (such as animation, etc.) are blocked, hogging CPU resources for long periods of time, and this can cause noticeable stuttering. The Stack Reconcilation cannot be put on hold, it cannot be shred, it cannot effectively balance the execution order of the rendering and animation, and so on. This is why React 16 introduced Fiber. Here I draw on the picture of barren Mountain’s article for comparison:

  • React 15 uses recursion to compare VirtualDom trees, which requires changing nodes to be found and updated without any breaks. This process is called harmonizationReconcilationIn theReconcilationDuring this time, the browser has been hogging browser resources, resulting in user-triggered events that do not respond, and will cause frame drops, resulting in significant lag.



1.3 Why Fiber Reconcilation is Used

Before analyzing this, we can use the following example to experience the benefits of sharding execution. The example is to generate 10,000 nodes in different ways.

  • Work without interruption
  • Insert the nodes every 40 milliseconds, 100 times
<! doctypehtml>
<html lang="en">
 <head>
<script>
function randomHexColor(){
    return "#" + ("0000"+ (Math.random() * 0x1000000 << 0).toString(16)).substr(-6);
}
// 2 Option 1: create 10000 DOM nodes in one go
setTimeout(function() {
    var k = 0;
    var root = document.getElementById("root");
    for(var i = 0; i < 10000; i++){
        k += new Date - 0 ;
        var el = document.createElement("div");
        el.innerHTML = k;
        root.appendChild(el);
        el.style.cssText = `background:${randomHexColor()}; height:40px`; }},1000);

// Select one: Generate 10000 nodes and insert 100 nodes in batches for 100 times.
setTimeout(function () {
	var root = document.getElementById("root");
    function loop(n) {
        var k = 0;
        for (var i = 0; i < 100; i++) {
            k += new Date - 0;
            var el = document.createElement("div");
            el.innerHTML = k;
            root.appendChild(el);
            el.style.cssText = `background:${randomHexColor()}; height:40px`;
        }
        if (n) {
            setTimeout(function () {
                loop(n - 1);
            }, 40);
        }
    }
    loop(100);
}, 1000);

  </script>
 </head>
 <body>
  <div id="root"></div>
 </body>
</html>
Copy the code



Seamless insertion, obvious sense of frame loss, the average FPS is 1529ms



Time-sharing sharding inserts, which give browsers a chance to breathe, are common at 17ms per FPS.

According to the above comparison, it can be seen that compared with operating a large number of DOM nodes at one time, the DOM operation with batch delay can obtain better user experience. Ultimately, the main thread of the browser handles GUI rendering, event handling, JS execution, resource loading, layout, etc. You can only do one thing at a time. Given enough time to give the browser a break, the browser will also JIT our code and do hot code optimizations, some DOM manipulation, and internal reflow processing. Reflow is a performance black hole, and most elements of the page are likely to be rearranged.

React 16 incorporates Fiber Reconcilation. In order to give users a sense of speed and not allow a single process to constantly occupy the Reconcilation process, it allocates CPU resources with a reasonable scheduling strategy, using Fiber architecture to make Reconcilation processes interruptable, relinquishing CPU execution at the right time, and allowing browsers to respond to user interactions in a timely manner. This improves the browser response rate. Use the images from The Barren Hills article again to present a high performance comparison of the act16 version.

  • In synchronous mode, although React16 is asynchronous, it is not open to the public. You can use the experimental version to find out, and should see a qualitative improvement in performance in version 17



  • Optimized Concurrent mode



2. What is Fiber

Fiber, also known as a coroutine or Fiber, is different from threads in that it does not have concurrency or parallelism, but is a mechanism for ceding control to flow.

2.1 React Active Transfer mechanism

The React Fiber: andcoroutinesReact Fiber divides tasks into work orders with different priorities. In combination with a scheduling strategy, when a high-priority task occurs during rendering, the rendering process can be interrupted, giving control back to the browser for the high-priority task, and then resume rendering when the browser is idle.

Browsers have no concept of processes,taskThe boundaries are blurred and do not haveInterrupt/ResumeAnd there is no preemption mechanism, so we cannot interrupt an executing program. Therefore, can only takeTransfer of controlMechanism. The technical term isCooperative Scheduling.

This is a kind ofcontractScheduling requires that our programs and browsers be closely integrated and trust each other. The browser assigns us the execution time slice. After completing the task in accordance with the agreed time, we return the control to the browser. Everything is subject to the command and scheduling of the browser.



2.2 Scheduling Tasks using a Browser

React uses the time fragmentation strategy to divide tasks into different levels of work units and execute tasks in the idle time of the browser to ensure smooth UI operations. In the browser, there are two apis that enable high and low priority execution:

  • RequestAnimationFrame: Performs a high-priority task
  • RequestIdleCallback: Executes a low-priority task

Function detail link

2.2.1 requestAnimationFrame

RequestAnimationFrame is executed at the beginning of each frame and is typically used to draw complex animations. This is a high-priority task. Because it is executed at the beginning of each frame, we can simulate simple time sharding scheduling.

  • Parameters:
// Create 1000 tasks
const tasks = Array.from({length: 1000}, () = > () = > { console.log('task'); });// No 20ms to perform a sharding task at a time
const doTask = (index = 0) = > {
    const start = Date.now();
    let i = index;
    let end;
    do {
        // Execute the task
        tasks[i++]();
        // Obtain the end time of the current task
        end = Date.now();
    } while( i < tasks.length && end - start < 20);
    
    console.log('-- -- -- -- -- -- -- -- -- --'.'20ms boundary ');
    // Set a new sharding task
    if (i < tasks.length) {
        // Call requestAnimationFrame to perform the shard task
        requestAnimationFrame(doTask.bind(null, i)); }};// Call the task scheduler for the first time
requestAnimationFrame(doTask.bind(null.0));

// Execution result:
// 347 task
// ---------- 20ms boundary
// 393 task
// ---------- 20ms boundary
// 260 task
// ---------- 20ms boundary
Copy the code

Above we implemented a simple timesharding function with requestAnimationFrame that executes tasks every 20ms, but if tasks are added to us for less than 20ms or more, the execution time will be too much or not enough. At this point, we need to use requestIdleCallback.

2.2.2 requestIdleCallback

RequestIdleCallback is used when we are concerned with user experience and do not want some unimportant task (such as statistics reporting) to cause the user page to lag. This function is a low-priority task and it is not certain that the callback will be executed.

The web page we see is drawn frame by frame by the browser, and an FPS of 60 is generally smooth. If the FPS is in single digits, you can perceive significant lag. What does the browser do with each frame?



We can see that in each frame the browser handles the interaction, JS execution, requestAnimationFrame invocation, layout, drawing, and so on. If the browser is not doing much work for another 16ms (1000ms/60pfs), it has free time to execute the callback to requestIdleCallback.



When the page is not updated and the browser is idle, it takes a long time to execute requestIdleCallback. But it is also possible that the requestIdleCallback callback never executes when the browser is busy. That’s not what we want. Fortunately, requestIdleCallback has a second parametertimeout, will be executed after the expiration date. However, the join is not known until the expiration date, so the user can clearly perceive the lag phenomenon.

// Create 1000 tasks
const tasks = Array.from({length: 1000}, () = > () = > { console.log('task'); });// No 20ms to perform a sharding task at a time
const doTask = (index = 0, idleDeadline) = > {
    let i = index;
    do {
        // Execute the task
        tasks[i++]();
    } while( i < tasks.length && idleDeadline.timeRemaining() > 0); // The task execution time has not expired
    
    console.log('-- -- -- -- -- -- -- -- -- --'.'20ms boundary ');
    // It has expired. What do I need to do
    if (idleDeadline.didTimeout) {
        console.log('Expired', i);
    }
    // Set a new sharding task
    if (i < tasks.length) {
        // Call requestIdleCallback to perform the sharding task
        requestIdleCallback(doTask.bind(null, i), { timeout: 1000}); }};// Call the task scheduler for the first time
requestIdleCallback(doTask.bind(null.0), { timeout: 1000 }); // 1000ms expiration time. If the task is not executed, the task is executed immediately
Copy the code
DOM manipulation in the requestIdleCallback is not recommended because it may result in style recalculation or rearrangement (such as calling getBoundingClientRect immediately after DOM manipulation), which is difficult to predict, and may result in the callback timed out and dropping frames.Copy the code

2.3 Execution Unit

Fiber Another interpretation is called Fiber: a data structure or unit of execution. Treat it as an execution unit. React checks the remaining time after an execution unit is completed and cedes control if there is no time left. As mentioned earlier, when there is an update, the task is inserted into the Update Ue queue. For example, setState performs the update component, enqueueing the task, and the task waiting for the update is invoked via requestIdleCallback.

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

// 1. PerformWork will get a Deadline indicating the remaining time
function performWork(deadline) {

  // 2️. Loop out the tasks in updateQueue
  while (updateQueue.length > 0 && deadline.timeRemaining() > ENOUGH_TIME) {
    workLoop(deadline);
  }

  // 3️. If you cannot complete all tasks in this execution, request the browser to schedule again
  if (updateQueue.length > 0) { requestIdleCallback(performWork); }}Copy the code

PerformWork: fragments of tasks executed in segments. Call execution whenever there is free time. WorkLoop: Obtains the update task from updateQueue and executes it. Each execution unit detects whether there is any remaining time, and enters the next execution unit. If there is no remaining time, the site is saved and the task will be restored after the execution right is obtained next time.

// Save the current processing scene
let nextUnitOfWork: Fiber | undefined // Save the next unit of work to be processed
let topWork: Fiber | undefined        // Save the first work unit

function workLoop(deadline: IdleDeadline) { // Deadline: deadline
  UpdateQueue retrieves the next execution unit or restores the last interrupted execution unit
  if (nextUnitOfWork == null) {
    nextUnitOfWork = topWork = getNextUnitOfWork();
  }

  // Check the remaining time after each execution unit
  // If it is interrupted, the next execution will start from nextUnitOfWork
  while (nextUnitOfWork && deadline.timeRemaining() > ENOUGH_TIME) {
    // Let's look at formUnitofWork
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork, topWork);
  }

  // Submit work
  if(pendingCommit) { commitAllWork(pendingCommit); }}Copy the code



3. Fiber structure in React

As we mentioned above, React15 is a Stack Reconcilation, while React16 uses Fiber Reconcilation. Fiber is a linked list. Each VirtualDom node corresponds to a Fiber node.

/** * FiberNode constructor *@param Tag Is used to mark the type of the fiber node *@param PendingProps indicates the props data to be processed@param Key Uniquely identifies a Fiber node *@param Mode Specifies the mode of the fiber node */
function FiberNode(tag: WorkTag, pendingProps: mixed,key: null | string,mode: TypeOfMode) {
  // Instance
  this.tag = tag; // This is used to mark the type of fiber node. FunctionComponent | HostRoot | HostPortal....
  this.key = key; // Uniquely identifies a fiber node
  this.elementType = null; // ReactElement. Type, which is the first argument to call 'createElement'
  this.type = null; Step / / component resolved after return, is generally ` function ` | ` class ` | component of the module type
  this.stateNode = null; // For the rootFiber node, hang on fiterRoot. For Child Fiber, hang on the corresponding component instance

  // The following attributes create a single-linked list tree structure
  // The return attribute always points to the parent node
  // The child attribute always points to the first child node
  // Sibling attribute always points to the first sibling node
  this.return = null;
  this.child = null;
  this.sibling = null; . }Copy the code

A node corresponds to a fiber. Establish connections between multiple fiber nodes, and then generate a Fiber tree. Fiber tree is a single-linked tree, and the key attributes to build this tree are return,child, sibling.

  • Return: Each fiber has a value pointing to the parent node
  • Child: pointing to the current nodeThe first oneChild nodes
  • Sibling: Pointing to the current nodeThe first oneBrother nodes
  • StateNode: component or DOM corresponding to each fiber object
<div class="root">
    <h1 class="title">React header</h1>
    <div class="parent">
        <span>child1</span>
        <p>child2</p>
        <button>child3</button>
    </div>
</div>
Copy the code

3.1 Interrupt Recovery Mechanism

With a linked list structure, we can handle React tree nodes. Combined with the browser scheduling section and workLoop above, FiberNode is our unit of work. PerformUnitOfWork is responsible for performing operations on FiberNode, performing deep traversal and returning the next FiberNode work unit.

/ * * *@params Fiber Current node to process *@params TopWork is the root of this update */
function performUnitOfWork(fiber: Fiber, topWork: Fiber) {
  // Process this node
  beginWork(fiber);

  // If there are child nodes, the child node is the next to be processed
  if (fiber.child) {
    return fiber.child;
  }

  // There are no child nodes
  let temp = fiber;
  while (temp) {
    completeWork(temp);

    // To the top node, exit
    if (temp === topWork) {
      break
    }

    // The sibling node is the next to be processed
    if (temp.sibling) {
      return temp.sibling;
    }

    // No, go back uptemp = temp.return; }}Copy the code

In conjunction with workLoop, because of its linked list structure, even if the process is interrupted, we save the last execution unit and are able to resume the workflow exactly from the last Unfinished FiberNode.

3.2 Reconciliation and Commit

React in addition to Fiber work unit split, there is also a phase split, which is also very important transformation. In the Stack Reconcilaition, there is a diff and a COMMIT. Fiber Reconcilication is divided into two stages: the coordination stage: the DIff stage, which can be interrupted to identify all node changes (node additions, deletions, modifications, props modifications, etc.), which are known as side effects. The following life cycle is called (

  • constructor,
  • static getDerivedStateFromProps,
  • shouldComponentUpdate, render

Commit phase: The side effects calculated in the previous phase are updated at one time. The synchronization cannot be interrupted in this phase. Which of the following cycles is executed

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

The work performed in the coordination phase is not used to cause any visible changes, and the phase can be interrupted, resumed, and execution control ceded. Lifecycle functions can also be executed multiple times. The Commit phase cannot be interrupted. Because the various side effects changes (including DOM changes) are handled correctly, you must ensure that they are called only once in sequence because there are side effects.

End of 4

The reconciliation and COMMIT phases will be studied in detail later. After learning this, I finally have a clear understanding of Fiber architecture. This part is not learned by reading the source code, but obtained from others’ summary, because the understanding of Fiber architecture can better analyze the subsequent code. Special reference to the following articles:

  • This is probably the most popular way to open React Fiber

Later, I will learn the scheduling principle of React. In the process of learning the source code, it is really easy to slack off. I hope I can stick to it. Attached are all the links between learning.

  1. React source series 1: React API

  2. React Render FiberRoot

  3. React source series 3: React.Render process 2 update

  4. React Fiber

  5. React Scheduler Scheduler

  6. React Scheduler….