fiber

React renders components synchronously (” in one go “) from the start of setState to the end of rendering. If a large number of components need to be rendered, JS execution will occupy the main thread for a long time, resulting in poor responsiveness of the page and poor effect of React in animation, gesture and other applications.

To solve this problem, the React team rewrote the core algorithm of React — Reconciliation after two years of work. This new feature was released in v16. In order to distinguish between the before and after Reconciler, the former Reconciler is often referred to as a Stack Reconciler, and the rewritten reconciler is called Fiber, or fiber for short.

Caton reason

The workflow for the Stack Reconciler is much like the call to a function. The tune component in the parent component can be likened to a recursion of functions (which is why it is called a Stack Reconciler). After setState, React immediately begins the reconciliation process, starting with the Virtual DOM to find out the difference. After all the Virtual DOM is iterated, the Reconciler can give information about the real DOM that currently needs to be modified, which is passed to the Renderer for rendering, and then the update is displayed on the screen. For a very large vDOM tree, the reconciliation process can be very long (x00ms), during which the main thread is occupied by JS, so any interaction, layout, rendering stops, giving the user the impression that the page is stuck.

Scheduler

Scheduling is a fiber reconciliation process that determines what should be done and when. The process at 👆 shows that in the Stack Reconciler, reconciliation is “one piece at a time,” which is fine for functions, because we only want the results of the function to run, but for the UI there are the following issues to consider:

  • Not all state updates need to be immediately visible, such as updates in off-screen parts
  • Not all update priorities are the same, for example, a response entered by a user has a higher priority than a response populated by a request
  • Ideally, it should be possible to interrupt low-priority actions for some high-priority actions, such as user input while a comment on the page is still in reconciliation

Ideally, the reconciliation process would look something like the one below, where you do one small task at a time, catch your breath, and then go back to the main thread to see if there’s any higher-priority tasks to deal with. If you do the higher-priority tasks first, If there is no cooperative scheduling, the cooperative scheduling will continue.

Fiber tree & Fiber

Take a look at how react works under stack-Reconciler. React creates (or updates) elements in the code, creates (or updates) the Virtual DOM based on those elements, and then modifies the real DOM based on the difference between the Virtual DOM before and after the update. Note that under the Stack Reconciler, UPDATES to the DOM are synchronous, which means that during the virtual DOM comparison process, when an instance is found to be updated, the DOM operation is immediately executed.

In fiber-conciler, operations can be broken up into small parts and can be interrupted, so synchronizing the DOM may cause the fiber-tree to be out of sync with the actual DOM. For each node, it not only stores the basic information of the corresponding element, but also stores some information for task scheduling. Thus, fiber is just an object representing the smallest unit of work that can be split in the Reconciliation phase, corresponding to the React Instance in the figure above. Manage Instance’s own features through the stateNode attribute. The next unit of work of the current unit of work is represented by child and Sibling, and return indicates the target to be merged when the result is returned after the processing is complete, usually pointing to the parent node. The entire structure is a linked list tree. After each unit of work (fiber) completes, it checks to see if it still has the main thread time slice, if it continues to the next one, if it does not, it first processes other high-priority transactions, and continues to execute when the main thread is idle.

fiber {
  	stateNode: {},
    child: {},
    return: {},
    sibling: {},}Copy the code

For example

The current page contains a list that renders a button and a set of items that contain a div containing numbers. You can square all the numbers in the list by clicking on the button. There is also a button that you can click to adjust the font size.

Once the page is rendered, it initializes into a fiber-tree. Initializing a Fiber-tree is no different from initializing a Virtual DOM tree, which I won’t repeat here.

React also maintains a workInProgressTree. WorkInProgressTree is used to compute updates and complete the reconciliation process.

When the user clicks the square button and calls setState with the squared list of each element, React will place the current update in the list’s update queue. React, however, does not immediately compare and modify the DOM. Instead, scheduler processes it.

Scheduler processes the update based on the current main thread usage. To implement this feature, the requestIdelCallbackAPI is used. React adds Pollyfill to browsers that don’t support this API.

Generally speaking, client threads perform tasks in the form of frames, and most devices are controlled at 30-60 frames without affecting the user experience; The main thread usually has a short Idle Period between execution frames. RequestIdleCallback can call IdleCallback during this Idle Period to perform some tasks

  1. Low priority tasks are performed byrequestIdleCallbackProcessing;
  2. High priority tasks, such as animation related byrequestAnimationFrameProcessing;
  3. requestIdleCallbackIdle period callbacks can be invoked in multiple idle periods to perform tasks;
  4. requestIdleCallbackMethod to provide deadline, that is, task execution limit time, in order to split tasks, avoid long time execution, blocking UI rendering and resulting in frame drop;

Once the Reconciliation process gets the time slice, it enters the Work loop. The Work loop mechanism allows React to switch between calculating and waiting states. To do this, two things need to be tracked for each loop: the next unit of work (the next fiber to be processed); It is currently able to occupy the main thread time. The first loop, the next unit to be processed is the root node.

Since the update queue on the root node is empty, copy the root node directly to the workInProgressTree from the fiber-tree. The root node contains Pointers to child nodes (lists).

The root node has no update operations, and according to its child pointer, the List node and its corresponding Update queue are then copied into the WorkinProgress as well. After the List is inserted, it is returned to its parent node to indicate that processing of the root node is complete.

After the root node is processed, react checks whether the time slice is used up. If not, start processing the next node List based on the information it holds for the next unit of work.

The List contains updates, so react calls updater funciton passed in setState to get the latest state value, which should be [1,4,9]. Normally we pass in an object when we call setState now, but with Fiber conciler we have to pass in a function that returns the state to be updated. React has supported this approach since early versions, but it’s generally not used. In later versions of React, direct object writing may be deprecated.

setState({}, callback); // stack conciler
setState((a)= > { return {} }, callback); // fiber conciler
Copy the code

After getting the latest state value, React updates the List’s state and props values, calls Render, and gets a set of elements generated from the updated List. React determines whether Fiber is reusable based on the type of elements generated. For the current situation, the type of the newly generated Elments remains the same (still Button and Item), so React will copy the corresponding fibers of these elements directly from the fiber-tree into the workInProgress. And tag the List, because this is a node that needs to be updated.

After the List node is processed, React still checks whether the current time slice is sufficient. If enough, then the next one, button, is processed. At this point, the user clicks the button to enlarge the font. This zoom operation is implemented purely by JS and has nothing to do with React. However, the action does not take effect immediately because the React timeslice is still running, so we still need to process button.

Button does not have any child nodes, so you can return at this point and mark button processing complete. If the button changes, you need to tag it, but the current situation does not, you just need to tag it.

The old rule is to see if you have enough time to process a node. Note that the font enlargement operation is already waiting to free the main thread.

So let’s do the first item. The shouldComponentUpdate hook determines whether the props passed in need of change. For the first Item, it’s 1 before and after the change, so it doesn’t change, shouldComponentUpdate returns false, copy the div, finish processing, check the time, if there’s still time to enter the second Item.

The second Item shouldComponentUpdate returns true, so we need to tag it, duplicate div, call render, update the contents of div from 2 to 4, because div is updated, so we need to tag div. The current node is processed successfully.

In this case, the div is already a leaf node, does not have any siblings, and its value has been updated. In this case, you need to merge the effect resulting from this node change into the parent node. React maintains a list of all the elements that produce effects.

After the merge, we go back to the parent Item, and the parent tag is complete.

The next unit of work is Item. Before entering the Item, check the time. But by this time time had run out. React must swap the main thread and tell the main thread to allocate time for the rest of the operation.

The main thread then enlarges the font. React/workinTree/WorkinTree/fiber-tree

When done, Item returns to List and merges effect. Effect List now looks like this:

At this point the List returns to the root node and merge effect, and all nodes can be marked complete. React now marks workInProgress as pendingCommit. This means you are ready to enter the COMMIT phase.

At this point, you need to check if you have enough time, and if not, wait until you have time to commit the changes to the DOM. After phase 2, the reacDOM updates the DOM based on the effect-list calculated in phase 1.

After updating the DOM, workInProgress is completely aligned with the DOM. To keep the current fiber-tree and DOM consistent, React swaps the current and workInProgress Pointers.

In fact, React maintains two trees (double-buffering) most of the time. This reduces the time it takes to allocate memory and clean up garbage the next update. After commit, execute componentDidMount.

summary

By breaking up the reconciliation process into small units of work, the page can be more responsive to browser events. However, another problem is that if the react rendering is taking a long time, it will still block the react rendering. That is why Fiber Reconciler has added a prioritization strategy.

priority

module.exports = {
  NoWork: 0.// No work is pending.
  SynchronousPriority: 1.// For controlled text inputs. Synchronous side-effects.
  AnimationPriority: 2.// Needs to complete before the next frame.
  HighPriority: 3.// Interaction that needs to complete pretty soon to feel responsive.
  LowPriority: 4.// Data fetching, or result from updating stores.
  OffscreenPriority: 5.// Won't be visible but do the work in case it becomes visible.
};
Copy the code

The core of the priority strategy is that during the reconciliation phase, lower-priority operations can be interrupted by higher-priority operations and the main thread can perform higher-priority updates for faster user-perceived response. It is important to note that when the main thread is reassigned to a lower-priority operation, it does not start from the state where it was last worked, but from scratch.

This can lead to two problems:

  • Starve: The scheme being tested is reuse, which means that high-priority operations can be reused if they do not modify nodes that lower-priority operations have already completed.
  • A render may call the declaration cycle function more than once

Life cycle function

In some cases, the lifecycle function of the Phase1 phase may be executed more than once. For example, when a low-priority componentWillUpdate execution is interrupted by a higher-priority execution, a higher-priority execution is completed, and a lower-priority operation is returned, componentWillUpdate may be executed again. For some cases where you expect to execute only once, or where you need to perform a symmetric operation between two lifecycle functions, consider this case to make sure you don’t crash the entire App.

reference

Reconciliation

Fiber reconciler

Cooperative scheduling

Lin Clark presentation in ReactConf 2017