1. Browser rendering basics

1.1 apply colours to a drawing frame

Frame: During animation, each still frame is called a frame

Frame per second: The number of still images played continuously per second

Frame running Time: the dwell time of each still picture

Dropped Frame: When a frame is longer than the average frame length

  • Generally speaking, the browser refresh rate is 60Hz, and the rendering time of a frame must be controlled at 16.67ms(1s/60=16.67ms).
  • If the rendering time is longer than that, the user will visually experience stutter, namely frame loss (dropped frame)

1.2 frame life cycle

Simply describe the frame life cycle

  1. Processing input events first gives the user the fastest feedback

  2. The next step is to deal with the timer, which needs to check whether the timer is running out of time and execute the corresponding callback function

  3. Next, process the Begin Frame, which is the event for each Frame, including window.resize, Scroll, and so on

  4. The requestAnimationFrame requestAnimationFrame(rAF) is then executed, and each time before drawing, the rAF callback is executed

  5. Then perform the Layout operation, calculate the layout and update the layout, that is, what is the style of the element, how should be displayed on the page

  6. Paint gets the size and position of each node in the tree, and the browser fills in the content for each element

The RequestIdleCallback function performs tasks registered in the RequestIdleCallback function (which is the basis of the React Fiber task scheduling implementation) if the Idle Period remains.

1.3 Frame loss experiment

Why do I lose frames?

For smooth animation, if the processing time for a frame exceeds 16.67ms, you can feel the lag. The link below is a simulated frame loss experiment

Demo: linjiayu6. Making. IO/FE – RequestI…

When the user clicks any key A, B or C, the main thread performs the click Event task and the browser cannot process the next frame in time, leading to the phenomenon of lag. The main logic codes are as follows

// Process synchronization tasks and occupy the main thread

const bindClick = id= >

element(id).addEventListener('click', Work.onSyncUnit)

// Bind the click event

bindClick('btnA')

bindClick('btnB')

bindClick('btnC')

var Work = {

// There are 10,000 tasks

unit10000.// Process each task

onOneUnitfunction (for (var i = 0; i <= 500000; i++) {} },

// Process all tasks synchronously

onSyncUnitfunction ({

let _u = 0

while (_u < Work.unit) {

Work.onOneUnit()

_u ++

}

}

}
Copy the code

1.4 Solve the frame problem

As mentioned earlier, when the normal frame task is completed within 16ms, there will be idle time to execute the task registered in the requestIdleCallback function. This is the basic API implemented in React Fiber. Let’s take a look at the call to requestIdleCallback in each frame

  1. Low-priority tasks are handled by requestIdleCallback

  2. High-priority tasks such as those related to animation are handled by requestAnimationFrame

  3. RequestIdleCallback can be called during multiple idle periods to perform tasks

  4. Window. RequestIdleCallback (the callback) will accept the default parameters in the callback, deadline, which includes the down to two attributes:

    • timeRaminingReturns how much time is left for the current frame
    • didTimeoutreturncallbackWhether the task times out

Now let’s transform the previous experiment:

  1. Use RequestIdleCallback to process tasks

  2. Highly time-consuming tasks are disassembled and executed step by step in idle period

The logic is as follows:

const bindClick = id= >

element(id).addEventListener('click', Work.onAsyncUnit)

// Bind the click event

bindClick('btnA')

bindClick('btnB')

bindClick('btnC')

var Work = {

// There are 10,000 tasks

unit10000.// Process each task

onOneUnitfunction (for (var i = 0; i <= 500000; i++) {} },

// Asynchronous processing

onAsyncUnitfunction ({

// Free time 1ms

const FREE_TIME = 1

let _u = 0

function cb(deadline{

// The amount of time left in a frame > 1ms when the task is not finished

while (_u < Work.unit && deadline.timeRemaining() > FREE_TIME) {

Work.onOneUnit()

// Count the number of times executed, break out of the loop when 10000, if not 10000 for the following judgment, continue to put into idle execution

_u ++

}

// Task complete, execute callback

if (_u >= Work.unit) {

// Perform the callback

return

}

// If the task is not completed, continue to wait for idle execution

window.requestIdleCallback(cb)

}

// At the beginning of the click, the time-consuming task is put into the idle time

window.requestIdleCallback(cb)

}

}
Copy the code

The effect is as follows:

You can see that the frame rate is around 60fps.

It is worth noting that lengthy tasks should be avoided in the requestIdleCallback, otherwise they may block the page rendering

If a long task is executed in the requestIdleCallback, it will steal some time from the second frame after the first frame runs out of time, resulting in stalling.

React Fiber architecture

Disadvantages of the Act15 architecture:

  1. Update the tree recursively, without interruption

  2. High priority user actions such as click event animations must wait for the main thread to be released before responding, resulting in frame loss

To address these issues, the React team reconstructed the core algorithm, which produces Fiber Reconciler, in the following process:

Note: The Scheduler and Fiber Reconciler periods are interruptible, while the COMMIT period is non-interruptible

React fiber architecture changes:

  1. The structure of the tree is reconstructed into a multi-necklace list structure and the recursive algorithm is reconstructed into a depth-first traversal algorithm

  2. Refactor an uninterruptible update into an asynchronous interruptible update

  3. Defragment the update and check if there are any high-priority tasks after each part is executed. If so, record the status and execute the high-priority tasks first

Execute the rest at the next free time

2.1 Scheduler (Scheduling)

ShouldYieldToRenderer shouldYieldToRenderer shouldYieldToRenderer shouldYieldToRenderer shouldYieldToRenderer shouldYieldToRenderer shouldYieldToRenderer shouldYieldToRenderer shouldYieldToRenderer Wait for the next requestIdleCallback callback to continue.

// Flush asynchronous work until there's render a higher priority event
while(nextUnitOfWork ! = =null && !shouldYieldToRenderer()) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
Copy the code

The schematic diagram is as follows:

Each unit of work runs with six priorities:

  • synchronousSynchronous execution
  • taskExecute before next tick
  • animationExecute before the next frame
  • highImmediate implementation in the near future
  • low A slight delay (100-200ms) in execution is also fine
  • offscreen The next render time or scroll time is executed

Synchronous first screen (first render) is used and requires as fast as possible, whether it blocks the UI thread or not.

The animation is scheduled with a requestAnimationFrame so that the animation process starts immediately on the next frame;

The last three are all performed by the requestIdleCallback callback;

Offscreen refers to the currently hidden, off-screen (invisible) element

High priority things like keyboard input (wanting immediate feedback), low priority things like web requests, making comments appear, and so on.

Each priority will be assigned a certain expirationTime. The shorter the time, the higher the priority.

React16’s expirationTimes model can only differentiate if >=expirationTimes determines whether a node is updated.

There are two problems with this prioritization mechanism:

  • How does the lifecycle function execute (which may be interrupted frequently) : is the sequence of firing times not guaranteed

  • Starvation (low-priority starvation) : If there are many high-priority tasks, low-priority tasks will not be executed.

React17’s Lanes model can select an update interval and dynamically add or subtract priorities to the interval to handle more fine-grained updates.

2.2 Fiber Reconciler (Coordination Phase)

This process is the process of diff, as well as the process of effect collection, to find out the changes of all nodes, such as node addition, deletion, attribute change, etc. These changes are collectively called side effects. The final result is to generate an effect list, and in the following render, Render in the Commit phase according to the Effect List

2.2.1 Fiber Node Properties

Due to time-sharded updates, more contextual information is required. When switching high-priority tasks, remember the current node information so that the task can be continued at the next idle time

There are a lot of attributes in the fiber node, including return, child, and Sibling. StateNode; EffectTag; ExpirationTime; Alternate; nextEffect

class FiberNode {
constructor(tag, pendingProps, key, mode) {
  // Instance properties
  this.tag = tag; // Mark different component types, such as function component, class component, text, native component...
  this.key = key; // The react element key is the same as the JSX key, which is the final ReactElement key
  this.elementType = null; // The first argument to createElement, type on ReactElement
  this.type = null; // Represents the actual type of fiber. ElementType is basically the same, but may be different if lazy loading is used
  this.stateNode = null; // The instance object, such as the class component new, is mounted on this property, which is FiberRoot if it is RootFiber, or dom object if it is native
  // fiber
  this.return = null; // Parent node, pointing to the previous fiber
  this.child = null; // The child node points to the first fiber below itself
  this.sibling = null; // Sibling component, pointing to a sibling node
  this.index = 0; If there are no siblings, each child is given an index, and the index and key are diff together
  this.ref = null; // ref attribute on reactElement
  this.pendingProps = pendingProps; / / new props
  this.memoizedProps = null; / / the old props
  this.updateQueue = null; // A single setState execution on the update queue on fiber will attach a new update to the property, and each update will eventually form a linked list, which will eventually be updated in batches
  this.memoizedState = null; // For memoizedProps, the state rendered last time is equivalent to the current state, understood as the relationship between prev and next
  this.mode = mode; // Represents how the children of the current component are rendered
  // effects
  this.effectTag = NoEffect; // Specifies the current fiber update (update, delete, etc.)
  this.nextEffect = null; // Point to the next fiber update
  this.firstEffect = null; // Point to the first of all child nodes in fiber that needs to be updated
  this.lastEffect = null; // Point to the last fiber of all the child nodes that needs to be updated
  this.expirationTime = NoWork; // Expiration time, which represents at what point in the future the task should be completed
  this.childExpirationTime = NoWork; // child expiration time
  this.alternate = null; // References between the current tree and the workInprogress tree}}Copy the code

2.2.2 Traversing the Flow

React 16 makes extensive use of data structures such as linked lists. Replacing the previous tree structure with a multi-way linked list

The process is as follows:

  1. You start with the vertices

  2. We have child nodes, so we iterate over the child nodes

  3. If there are no children, see if there are any siblings. If there are, traverse the siblings and merge Effect upward

  4. If there are no siblings, see if the parent has any siblings, and if so, traverse the parent’s siblings

  5. If none, the traversal ends

2.2.2 Chain of side effects

When updating the workInProgress (WIP) tree, the tree collects side effects for each fiber node as it builds. When the WIP tree is completed, The nodes with side effects are formed into a single linked list of side effects through firstEffect, lastEffect and nextEffect. At last, update is completed through the side effects chain in the rendering stage


<div id="A1">
  A1
  <div id="B1">
    B1
    <div id="C1">C1</div>
    <div id="C2">C2</div>
  </div>
  <div id="B2">
    B2
  </div>
</div>

Copy the code

The above code creates the following chain of side effects

2.2.2 Reconciliation process

This is the Reconciliation state, with the old tree on the left and the WIP tree on the right. For the nodes that need to be changed, effectTag information is marked. In the final render phase, the changes are applied according to the effectTag. The specific process is as follows:

  1. If the current node does not need to be updated, clone the child node and jump to 5. Tag it if you want to update it

  2. Update the current node state (props, state, context, etc.)

  3. ShouldComponentUpdate (), false, jump to 5

  4. Call Render () to get the new child node and create fiber for the child node (the creation process will reuse the existing fiber as much as possible, the addition and deletion of the child node will also happen here)

  5. If no child fiber is generated, the unit of work ends, merge the Effect list to return, and use the sibling of the current node as the next unit of work. Otherwise, make child the next unit of work

  6. If there is no time left, wait until the next main thread is idle before starting the next unit of work; Otherwise, start right away

  7. If there is no next unit of work (back to the root of the workInProgress Tree), phase 1 ends and the pendingCommit state is entered

Actually 1-6 is the task loop, and 7 is the final result. At the end of the task loop, the effect list on the root node of the WIP tree is all side effects collected (since each one merges upward).

So, the process of building the workInProgress Tree is the process of diff, scheduling a set of tasks through the requestIdleCallback, coming back after each task to see if there are any queue-jumping (high-priority tasks), and returning time control to the main thread after each set of tasks. Continue building the workInProgress Tree until the next requestIdleCallback callback

2.2.2 Dual cache technology

When the build is complete, there are two trees: It is called current Fiber Tree and workInProgress Fiber Tree respectively. It can be seen that during construction, the WIP tree is modified based on the current Fiber Tree template, and the new Fiber tree is obtained. The current pointer then points to the WIP tree

Put aside the old Fiber tree, their fiber nodes are connected to each other through the alternate property, and the old fiber is used as the reserved space for the update of the new fiber to achieve the purpose of multiplexing the fiber instance. The benefits are:

  • Ability to reuse internal objects (Fiber)
  • Save time for memory allocation and GC

2.3 Commit (Render Phase)

This phase is uninterrupted execution:

  1. Process the Effect List, acting on the change information (including three processes: updating the DOM tree, calling component lifecycle functions, and updating internal state such as ref)

  2. When the processing is complete, commit the changes to the DOM tree

This process is synchronous, and do no heavy work during the life of the process

The build process lifecycle functions can be divided into two classes

// Phase 1 render/reconciliation

componentWillMount

componentWillReceiveProps

shouldComponentUpdate

componentWillUpdate

// Phase 2 commit

componentDidMount

componentDidUpdate

componentWillUnmount

References:

  1. # Lin Clark – A Cartoon Intro to Fiber – React Conf 2017

  2. Github.com/acdlite/rea…

  3. www.readfog.com/a/164644909…

  4. www.readfog.com/a/163598702…

  5. Mp.weixin.qq.com/s/gz7_StDD1…

  6. Segmentfault.com/a/119000003…

  7. Juejin. Cn/post / 684490…