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
-
Processing input events first gives the user the fastest feedback
-
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
-
Next, process the Begin Frame, which is the event for each Frame, including window.resize, Scroll, and so on
-
The requestAnimationFrame requestAnimationFrame(rAF) is then executed, and each time before drawing, the rAF callback is executed
-
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
-
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
unit: 10000.// Process each task
onOneUnit: function () { for (var i = 0; i <= 500000; i++) {} },
// Process all tasks synchronously
onSyncUnit: function () {
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
-
Low-priority tasks are handled by requestIdleCallback
-
High-priority tasks such as those related to animation are handled by requestAnimationFrame
-
RequestIdleCallback can be called during multiple idle periods to perform tasks
-
Window. RequestIdleCallback (the callback) will accept the default parameters in the callback, deadline, which includes the down to two attributes:
timeRamining
Returns how much time is left for the current framedidTimeout
returncallback
Whether the task times out
Now let’s transform the previous experiment:
-
Use RequestIdleCallback to process tasks
-
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
unit: 10000.// Process each task
onOneUnit: function () { for (var i = 0; i <= 500000; i++) {} },
// Asynchronous processing
onAsyncUnit: function () {
// 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:
-
Update the tree recursively, without interruption
-
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:
-
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
-
Refactor an uninterruptible update into an asynchronous interruptible update
-
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:
synchronous
Synchronous executiontask
Execute before next tickanimation
Execute before the next framehigh
Immediate implementation in the near futurelow
A slight delay (100-200ms) in execution is also fineoffscreen
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:
-
You start with the vertices
-
We have child nodes, so we iterate over the child nodes
-
If there are no children, see if there are any siblings. If there are, traverse the siblings and merge Effect upward
-
If there are no siblings, see if the parent has any siblings, and if so, traverse the parent’s siblings
-
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:
-
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
-
Update the current node state (props, state, context, etc.)
-
ShouldComponentUpdate (), false, jump to 5
-
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)
-
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
-
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
-
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:
-
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)
-
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:
-
# Lin Clark – A Cartoon Intro to Fiber – React Conf 2017
-
Github.com/acdlite/rea…
-
www.readfog.com/a/164644909…
-
www.readfog.com/a/163598702…
-
Mp.weixin.qq.com/s/gz7_StDD1…
-
Segmentfault.com/a/119000003…
-
Juejin. Cn/post / 684490…