Long warning: the original
React Fiber Performance Optimization (Internal Demo)The full edition of”

Performance optimization is a systematic project. If you only see the part, introduce the algorithm as soon as possible. But on the whole, the introduction of caching at key points can kill multiple algorithms in seconds, or explore the nature of events in a different way, but users may not want fast…

React16 features a new architecture called Fiber, whose biggest mission is to address the performance issues of the big React project, along with some of the pain points.

Pain points

The main ones are as follows:

  • A component can’t return an array. The most common case is when you can only use LI for UL elements and TD or TH for TR elements. When you have a component loop that generates a list of LI or TD, you don’t want to put a DIV in there because it breaks the semantics of the HTML.
  • Popup window problem, unstable unstable_renderSubtreeIntoContainer have been used before. Pop-ups rely on the context of the original DOM tree, so the API takes the component instance as its first argument, and then goes up the hierarchy to get the corresponding virtual DOM. Its other parameters work fine, too, but this method has never been converted…
  • For exception handling, we want to know which component is failing. Although React DevTool is available, it is still difficult to find the deep component tree. I wish there was a way to tell me where something went wrong and give me a chance to do some repair work when something went wrong
  • The popularity of HOC brings two problems. After all, it is a community-initiated scheme that does not take into account the downward transmission of ref and context.
  • Component performance optimization is all human, and mainly focused on the SUC, hopefully the framework can do something, even without SCU, performance can be up.

To solve the progress

  • 16.0 Solve the array problem by allowing components to return any array type; Release createPortal API to solve popover problem; New componentDidCatch hook for componentDidCatch (componentDidCatch, componentDidCatch, componentDidCatch)
  • 16.2 Introduces the Fragment component, which can be seen as a syntactic sugar for arrays.
  • 16.3 createRef and forwardRef are introduced to solve the transfer problem of Ref in HOC, and new Context API is introduced to solve the transfer problem of HOC Context (mainly SCU)
  • Performance issues, from 16.0 onwards, have been guaranteed by internal mechanisms involving batch updates and time-sharded limited updates.

A little experiment

We can peek into the optimization ideas of Act16 through the following experiment.

function randomHexColor(){
    return "#" + ("0000"+ (Math.random() * 0x1000000 << 0).toString(16)).substr(-6);
}
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);
Copy the code

This is a 10,000-node insert operation with innerHTML and style Settings that takes 1,000 ms.


Let’s improve it again, allocate times to insert nodes, each operation is only 100 nodes, a total of 100 times, found the performance of the exception is good!

function randomHexColor() { return "#" + ("0000" + (Math.random() * 0x1000000 << 0).toString(16)).substr(-6); } var root = document.getElementById("root"); setTimeout(function () { function loop(n) { var k = 0; console.log(n); 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);Copy the code

The reason for this is that browsers are single-threaded, putting GUI rendering, timer handling, event handling, JS execution, and remote resource loading all together. When you do something, you have to finish it before you can do the next thing. If we have enough time, the browser will JIT and hot optimize our code, and some DOM operations will be handled internally with reflow. Reflow is a performance black hole, and most elements of the page are likely to be rearranged.

How the browser works

Render -> Tasks -> Render -> Tasks -> Render -> Tasks ->….

Some of these tasks are controllable and some are not. For example, it is hard to say when setTimeout will be executed. It is always inaccurate. The resource loading time is not controllable. However, we can control some JS, let them dispatch execution, tasks should not be too long, so that the browser has time to optimize JS code and fix reflow! Below is our ideal rendering process


In summary, give your browser a good rest and it will run faster.

How do I disconnect and reconnect code

JSX is a happy surprise egg that meets your two wishes at once: componentization and tagging. And JSX becomes the standardized language for componentization.

<div>
   <Foo>
      <Bar />
   </Foo>
</div>
Copy the code

But tagging is a naturally nested structure, meaning it will eventually compile into code that executes recursively. That’s why the React team called the pre-React scheduler the stack scheduler. There’s nothing wrong with stacks. Stacks are easy to understand and have a small amount of code. According to our experiment above, we need to re-execute after break, and we need a linked list structure.

Linked lists are asynchronously friendly. The linked list does not have to go into recursive functions every time it loops, regenerating any execution context, variable objects, and activating objects, which of course performs better than recursion.

So Reat16 tries to update the components recursively into a sequential execution of a linked list. If the page has multiple virtual DOM trees, store their roots in an array.

Reactdom. render(<A />, node1) reactdom. render(<B />, node2) // If node1 is not included with node2, then the page has two virtual DOM treesCopy the code

If you read the source code carefully, React is actually a three-tier library. In Act15 there is a virtual DOM layer, which describes only structure and logic; The internal component layer, which is responsible for updating components, deals with reactdom. render, setState, and forceUpdate, allowing you to setState multiple times, perform only one real render, and execute your component instance’s lifecycle hook when appropriate. At the bottom rendering layer, different display media have different rendering methods, such as the browser side, which uses element nodes and text nodes. At the Native side, oc and Java GUI will be called. In canvas, there are special API methods.

The virtual DOM is a translation from JSX, whose entry function is react. createElement. There is not much room to manipulate it, and the third underlying API is very stable, so we can only change the second layer.

Act16 changed the internal component layer to a Fiber data structure, so its architecture name was changed to Fiber architecture. The Fiber node has return, child and Sibling attributes corresponding to the parent node, the first child and its sibling on the right. With these attributes, it is enough to turn a tree into a linked list and realize deep optimization traversal.


How do I determine the number of updates per update

In Act15, each update starts from the root component or the component after setState, and updates the entire subtree. The only way we can do this is to use an SUC on a node to break a portion of the update, or to optimize the efficiency of the SUC.

React16 requires the virtual DOM to be converted into Fiber nodes, first specifying a period of time, and then updating as many FiberNodes as can be converted during that period.

So we need to split our update logic into two phases. The first phase is to convert the virtual DOM to Fiber, and Fiber to component instances or real DOM (instead of inserting a DOM tree, inserting a DOM tree reflow). The conversion of Fiber and Fiber will obviously take time, and it is necessary to calculate how much time is left. And converting an instance requires calling some hooks, such as componentWillMount. At this time is called componentWillReceiveProps shouldComponentUpdate, componentWillUpdate, at this time will be time-consuming.

To give readers an idea of how React Fiber works, we’ll briefly implement reactdom.render, but we don’t guarantee it will work.

First, some simple ones:

var queue = [] ReacDOM.render = function (root, container) { queue.push(root) updateFiberAndView() } function getVdomFormQueue() { return queue.shift() } function Fiber(vnode){for(var I in vnode){this[I] = vnode[I]} this.uuid = math.random ()} Function toFiber(vnode){if(! vnode.uuid){ return new Fiber(vnode) } return vnode }Copy the code

UpdateFiberAndView to implement React time sharding, we use setTimeout simulation first. We’re not going to worry about how updateView is implemented for the moment, but maybe it’s just updateComponentOrElement and put them in another queue that needs to come out and do insertBefore componentDidMount!

function updateFiberAndView() { var now = new Date - 0; var deadline = new Date + 100; UpdateView () // Updates the view, which takes time, If (new Date < deadline) {var fiber = vdom, FirstFiber var hasVisited = {} do {firstFiber = toFiber(); / / A place if (! firstFiber){ fibstFiber = fiber } if (! HasVisited [fiber.uuid]) {hasVisited[fiber.uuid] = 1 // Instantiate the component according to fiber.type or create the real DOM // This will take time, Check time updateComponentOrElement(Fiber); If (fiber.child) {if (newdate-0 > deadline) {queue.push(fiber.child) {if (newdate-0 > deadline) {queue.push(fiber.child); Continue // Let the logic run back to A, constantly converting child, child.child, child.child.child}} // If the component has no children, If (fiber.sibling) {fiber = fiber.sibling; Continue / / let logic runs back to A place} / / up find fiber = fiber. Return the if (fiber = = = fibstFiber | |! fiber){ break } } while (1) } if (queue.length) { setTimeout(updateFiberAndView, 40) } }Copy the code

There is a do while loop, which carefully timed each time and put the nodes in the queue that have not been processed in time.

UpdateComponentOrElement looks like this:

function updateComponentOrElement(fiber){ var {type, stateNode, props} = fiber if(! stateNode){ if(typeof type === "string"){ fiber.stateNode = document.createElement(type) }else{ var context = Fiber. StateNode = new type(props, Context)}} if(statenode.render){// execute componentWillMount; children = statenode.render ()}else{children = Fiber. Childen} var prev = null; For (var I = 0, n = children. Length; i < n; i++){ var child = children[i]; child.return = fiber; if(! prev){ fiber.child = child }else{ prev.sibling = child } prev = child; }}Copy the code

So we have Fiber’s return, child, sibling, happy depth-first traversal.

How do I schedule my time so that it flows smoothly

There was a problem with updateFiberAndView. We allocated 100ms to update the view and the virtual DOM, and another 40ms to the browser to do other things. If our virtual DOM tree is small, we don’t really need 100ms; If the browser has more to do after our code, 40MS might not be enough. IE10 introduces setImmediate, requestAnimationFrame, and other new timers that allow the front-end, the browser, to make the page run more smoothly.

Browsers themselves are evolving, and as pages move from simple presentation to WebApps, they need new capabilities to host more node presentations and updates.

Here are some ways to help yourself:

  • requestAnimationFrame
  • requestIdleCallback
  • web worker
  • IntersectionObserver

We call this in turn browser-level frame control calls, idle calls, multi-threaded calls, and in-view calls.

RequestAnimationFrame is often used for animation and is used in new jQuery versions. The Web worker releases some packages at the start of Angular2 to experimentally diff data with. IntersectionObserver can be used in the ListView. RequestIdleCallback is an unborn face, and React officials like it.

UpdateFiberAndView has two time segments, one for you and one for the browser. RequestAnimationFrame helps us with the second period to ensure that the whole thing runs smoothly at 60 or 75 frames (which can be set in the operating system’s display refresh rate).

How does requestIdleCallback solve this problem


The callback has a parameter object. The object has a timeRemaining method, which is equivalent to new date-Deadline, and it is a high precision data, more accurate than milliseconds, at least how much time the browser has scheduled for updating the DOM and the virtual DOM. We don’t care. The second interval is not important, but the browser may take one or two seconds to execute the callback, so to be on the safe side, we can set the second parameter to execute 300ms after the callback ends. Trust the browser, because it’s written by the big boys, and the schedule is more efficient than yours.

So our updateFiberAndView could look like this:

Function updateFiberAndView(dl) {updateView() // Update the view, which takes time, If (dl.timeremaining () > 1) {var fiber = vdom, FirstFiber var hasVisited = {} do {firstFiber = toFiber(); / / A place if (! firstFiber){ fibstFiber = fiber } if (! HasVisited [fiber.uuid]) {hasVisited[fiber.uuid] = 1 // Instantiate the component according to fiber.type or create the real DOM // This will take time, Check time updateComponentOrElement(Fiber); If (fiber.child) {// If (dl.timeremaining () > 1) {queue.push(fiber.child)// Time is insufficient, put it into the stack break} fiber = fiber.child; Continue // let the logic run back to A, constantly converting child, child.child, child.child.child}} //.... } while (1)} if (queue.length) {requetIdleCallback(updateFiberAndView, {timeout:new Date + 100})}}Copy the code

This concludes the limited update to ReactFiber based on time sharding. React actually implements requestIdleCallback itself to take care of most browsers.

Batch update

But the React team decided it wasn’t enough and needed something more powerful. BatchedUpdates are created because some businesses don’t have a strong need to synchronize views in real time and want to run all the logic before updating the view. Currently, batchedUpdates is not a stable API, so you should use reactdom.unstable_batchedupDates like this.

How do you implement this thing? I’m just going to do an opening switch, and if I turn it on, I’m going to disable updateView.

var isBatching = false function batchedUpdates(callback, event) { let keepbook = isBatching; isBatching = true; try { return callback(event); } finally { isBatching = keepbook; if (! isBatching) { requetIdleCallback(updateFiberAndView, { timeout:new Date + 1 } } } }; Function updateView(){if(isBatching){return}Copy the code

React: Anujs: Anujs: Anujs: Anujs: Anujs: Anujs: Anujs: Anujs: Anujs: Anujs: Anujs: Anujs: Anujs

Github.com/RubyLouvre/…

React also makes extensive use of batchedUpdates internally to optimize user code, such as setState for event callbacks and setState for hooks (componentDidXXX) in the COMMIT phase.

It can be said that setState is a merged render of a single component and batchedUpdates is a merged render of multiple components. Merge rendering is the main optimization method for React.

Why use deep optimization traversal

React uses Fiber to change tree traversal to linked list traversal, but there are so many traversal methods, why DSF? !

This involves a classic message communication problem. If the parent is communicating, we can communicate through props, and the child component can hold references to the parent and call the parent at any time. React invented the context object because communication between multiple levels of components, or components with no containment relationships, was a problem.

Context starts out as an empty object, so we call it unmaskedContext for convenience.

When it encounters a component that has the getChildContext method, that method generates a new context, merges it with the previous one, and passes the new context down as unmaskedContext.

When it encounters a component with contextTypes, the context extracts a portion of the content for the component to instantiate. This partial context, we call it a maskedContext.

Components always cut meat from an unmaskedContext as their own context. Poor!!!

If the child component does not have contextTypes, it does not have any properties.

In Act15, in order to pass the unmaskedContext, most methods and hooks leave an argument to it. But a context of this size has no place in the document. At the time, the React team hadn’t figured out how to handle component communication, so the community had been coming to the rescue with the exotic Redux. This was true until Redux’s writers joined the React team.

Another concern is that it might be compared with a maskedContext rather than an unmaskedContext by the SCU.

So based on those questions, finally the new Context API comes out. First of all, unmaskedContext doesn’t go back and forth between methods like it used to, there’s a separate contextStack. You start by pushing an empty object, and when you get to a component that needs to be instantiated, you take it first. When the component is accessed again, it pops out of the stack. Therefore, we need depth-first traversal to ensure that every point node is visited twice.


The same goes for containers, which are the real parent nodes that we need for an element’s virtual DOM. In Act15, it’s going to be wrapped in a containerInfo object and it’s going to be layered.

As we know, virtual DOM is divided into two categories, one is component virtual DOM, whose type is function or class. It does not generate nodes itself, but generates component instances, and generates the next-level virtual DOM through the render method. One is the element virtual DOM, where type is the tag name and DOM nodes are generated. The stateNode of the upper element is the contaner of the lower element.

This independent stack mechanism effectively solves the problem of parameter redundancy of internal methods.

There was a problem, however, when the first rendering was done, the contextStack was empty. And then we’re in some component setState in the virtual DOM tree, and how do we get its context? The React solution is to start rendering from the root each time and skip unupdated nodes via updateQueue acceleration — each component creates an updateQueue property on top of it when it is in setState or forceUpdate. Anujs saves its previous unmaskedContext to the instance. An unmaskedContext can be viewed as a union of all the previous contexts, and one can be used for multiple purposes.

When we batch update, how many discrete sub-components may be updated, and one component between two of them uses SCU return false, this SCU should be ignored. So let’s reference some variables to make it transparent. Just as forceUpdate can make components SCU oblivious.

Why the life-cycle hook overhaul

React divides the virtual DOM update process into two phases, the Reconciler phase and the COMMIT phase. The Reconciler stage corresponded to earlier versions of the DIFF process, and the COMMIT stage corresponded to earlier versions of the Patch process.

Some mini-React, such as Preact, mix them together, diff on one side and patch on the other (thankfully it uses promise.then to optimize, ensuring that only one component is updated at a time).

Some React miniatures are optimized by reducing movement, so they use a variety of algorithms, including the shortest edit distance, the longest common subsequence, the longest rising subsequence…

In fact, algorithm-based optimization is a kind of desperate optimization, just like the Maya civilization remained in the Stone Age because they could not find copper ore, and the great craftsman spirit was born to polish the stone tools beautifully.


The reason for saying so, because the diff algorithm is used to compare the old and new children of components, children generally do not appear too long, a bit of cannon hit the mosquito. And when our applications get so big, with tens of thousands of components on the page, even the best algorithms can’t keep browsers from getting tired. They didn’t think the browser would get tired, and they didn’t think it would be a long-distance problem. If it’s a 100-meter dash, or a 1,000-meter race, of course, the faster the better. If it is a marathon, you need to consider the preservation of physical strength, need to pay attention to rest. Performance is a systematic engineering.

In our code, resting is checking the time and then breaking the Fiber chain.

In updateFiberAndView, the update of nodes is uncontrollable, so the test time is not completed until all updates are completed. And we don’t have to worry about the updateView at all, because the updateView is essentially inside batchedUpdates, which has a try catch in it. Update the node based on DFS. Check the time on each node. This process is very error-prone because the component calls hook/method (constructor, componentWillMount, render) three times during mounting. Components in the process of updating will adjust four hooks (componentWillReceiveProps shouldUpdate, componentWillUpdate), each method can’t use the try catch wrapped up, it would have been poor performance. Constructor, render is inevitable, so the knife to three willXXX.

In previous versions, componentWillMount with componentWillReceiveProps do internal optimization, perform multiple setState will be postponed to render a merger. So the user setState arbitrarily. These willXXX also allow the user to manipulate the DOM arbitrarily. Manipulating the DOM can cause reflow, which is officially undesirable. GetDerivedStateFromProps lets you set the new state in Render. If you return a new object, it will set the state for you. Since this is a static method, you can’t manipulate instance, which prevents you from manipulating setState multiple times. Since there is no instance, there is no instance.refs.xxx, and you have no chance to manipulate the DOM. This way, the logic of getDerivedStateFromProps should be simple so that there are no errors, no errors, and no interruptions to the DFS process.

GetDerivedStateFromProps replaced the original componentWillMount and componentWillReceiveProps method, and componentWillUpdate is dispensable, before is completely symmetrical for good.

Even in the coming asynchronous updates, the Reconciler stage can be executed multiple times before a COMMIT is executed, which also causes the willXXX hooks to be executed multiple times, violating their semantics, and their abandonment is irreversible.

When entering the Commi phase, the component has a new hook called getSnapshotBeforeUpdate, which executes only once like the hook in the COMMIT phase.

And if something goes wrong, after componentDidMount/Update, we can use the componentDidCatch method. So the whole process looks like this:


No hooks in the Reconciler stage should operate on the DOM, and it is best not to use setState, which we call lightweight hooks *. Hooks in the COMMIT phase are called weight hooks **.

Mission systems

UpdateFiberAndView is located in a requestIdleCallback, so it has limited time and less time allocated to the DFS section, so they can’t do much. What to do about this? Mark it up and leave it for the COMMIT phase. The result is a mission system.

As each Fiber is assigned a new task, a sideEffect is added by bit manipulation. SideEffect literally means sideEffect, very FP stream flavor, but we understand it as a task that is easier for us to understand.

Each Fiber can have multiple tasks, such as inserting a DOM or moving it around, with Replacement, styling, and Update.

How do I add tasks?

fiber.effectTag |= Update
Copy the code

How do YOU ensure that you don’t add the same tasks repeatedly?

fiber.effectTag &= ~DidCapture;
Copy the code

During the COMMIT phase, how do YOU know it includes a task?

If (fiber. EffectTag & Update){/*Copy the code

React has so many built-in tasks, from DOM manipulation to Ref processing to callback recall…


Anu’s task name, by the way, is multiplication and division based on prime numbers.

Github.com/RubyLouvre/…

Whether it’s a bit operation or a prime number, we just need to ensure that a Fiber task of the same nature is performed only once.

In addition, mission systems serve another purpose, ensuring that some tasks take precedence over others. We call this task sorting. This is just like the warehouse management of express delivery, which can be optimized with classification. For example, the insert and move of the element virtual DOM must be performed before all tasks, and the remove must be performed after componentWillUnmount. These missions are in this order because they make sense, have been carefully scrutinized by the pros, and validated by the masses in the Era of Act15.

The conjoined infant structure of Fiber

Conjoined twins is a scary term, and it’s uncomfortable to think about because Fiber is actually an unusual structure that my Anujs haven’t implemented well until now. Fiber has an attribute called alternate, which you call backup, fall guy, and stuntman. You can also think of it as the git development branch, and the stable one as the master. Each time the component instance stateNode has an _reactInternalFiber object on it, which is the Master branch, it immediately copies an identical alternate object dedicated to thunder.

The alternate object will accept the new props passed from above, and then get the new state from getDerivedStateFromProps, and then render a different sub-component, which then render. Gradually, the difference between master and alternate becomes larger and larger. When a child component fails, we roll back to the master branch of the boundary component.

React16 simulates git add, Commit, and Revert via Fiber.

For thoughts on the structure of Siamese twins, see my other article, from False Boundaries to Rollback to MWI, which I will not expand on here.

Middleware system

Speaking of middleware systems, you may be familiar with the Onion model in KOA and Redux.


Back in act15, there was already something called Transaction, which looked exactly like the Onion model. In the Transaction source code, there is a special ASCII diagram that visually explains what Transaction does.


Simply put, a Transaction is a Method that needs to be executed wrapped in a Wrapper and executed using the Perform method provided by Transaction. Before performing, initialize methods in all wrappers are executed; Perform all of the close methods after perform. A set of initialize and close methods is called a wrapper, and as you can see from the example above, Transaction supports multiple wrapper stacks.

What’s the use of this thing? When the DOM is updated, collect the current focus element and selection. When the DOM is updated, restore the focus and selection (because inserting a new node causes the focus to be lost, document.activeElement becomes body, or autoFocus changes the focus to another input. The cursor of the input we are typing is missing and cannot be typed normally. During the update, we need to save some of the uncontrolled components and restore the uncontrolled components after the update. (Uncontrolled components are a tricky topic, so that form elements without onChange cannot manually change their value.) Of course, the initial loading and emptying of contextStack, containerStack can also be used as middleware. Middleware is distributed on both sides of batchedUpdates, a very scalable design, why not use it more!

conclusion

React Fiber is a revolution for React, addressing the React project’s heavy reliance on manual optimization and delivering epoch-making performance optimization through system-level scheduling. The uncanny Fiber structure provides a fallback for abnormal boundaries and the next starting point for limited updates. The React team is full of talent, creativity, and ingenuity to tackle problems at a higher level than any other open source team. This is why I always choose to learn React.

But like everyone else, I initially struggled to learn the source code for Act16. Later, after watching the video of their team, I had a deep understanding of the linked list structure of time sharding and Fiber, and gradually understood the whole idea. The general process could be copied without breakpoint debugging of React source code. As the saying goes, it is better to see than to write (namely write anujs, welcome everyone to add star, github.com/RubyLouvre/…). Better to retell it to someone else. Hence the article.

* * * * * * * * * * * * * * * * * *

Conscious reward: Chin Cheng

* * * * * * * * * * * * * * * * * *