Author: Ge Xing

background

React implements the use of the Virtual DOM to describe the UI, minimizing the DOM update by comparing the differences between two trees. This makes user code stupid, but it also introduces some problems. The core problem is that diff calculation is not free. In the case of a large number of elements, the whole diff calculation process may take a long time, resulting in animation frame loss or difficult to respond to user operations, resulting in a decline in user experience.

There are two main reasons why this problem occurs:

  1. The React < 15 version has always rendered the UI in Stack Reconciler (which is called Stack Reconciler in contrast to Fiber Reconciler), The Stack Reconciler is implemented in a recursive manner, and we know that recursion cannot be interrupted. Every time there is a need for updates, React executes the diff from the node that needs to be updated all the way through, which consumes a lot of time.
  2. Browsers are multi-threaded, including the renderer thread and the JS thread, which are mutually exclusive, so UI responses are blocked when the JS thread takes up a lot of time.

Both of these reasons are necessary, because if JS executes, the UI will not block, and the user will not notice. Let’s take a look at some of the more common performance tuning techniques.

Common performance optimization tools

We generally use the following methods to optimize performance

Image stabilization

Optimize the function using anti-shake methods. This approach delays updating the UI until the user has finished typing. So the user doesn’t feel stuck when typing.

class App extends Component {
  onChange = () = > {
    if (this.timeout) {
      clearTimeout(this.timeout);
    }
    this.timeout = setTimeout(
      () = >
        this.setState({
          ds: []}),200
    );
  };
  render() {
    return (
      <div>
        <input onChange={this.onChange} />
        <list ds={this.state.ds} />
      </div>); }}Copy the code

Using PureComponent | | shouldComponentUpdate

Optimize with shouldComponentUpdate or PureComponent. This lets React skip unnecessary diff calculations by shallow comparing the props and state before and after.

class App extends Component {
  shouldComponentUpdate(nextProps, nextState) {
    return (
      !shallowEqual(nextProps, this.props) || ! shallowEqual(nextState,this.state)
    );
  }
  render() {
    return (
      <div>
        <input onChange={this.onChange} />
        <list ds={this.state.ds} />
      </div>); }}Copy the code

There are three points to note about this approach:

A. Shallow comparison is the only way to compare deeper objects. If you compare objects for longer than React diff, the loss is not worth the gain.

B. Object references. When assigning state, pay attention to object references, such as the following code will make the component unable to update

class App extends PureComponent {
  state = {
    record: {}};componentDidMount() {
    const { record } = this.state;
    record.name = "demo";
    this.setState({
      record,
    });
  }
  render() {
    return <>{this.state.record.name}</>; }}Copy the code

C. The execution value of the function is changed. This is the case when variables other than props and state are used in the function, and those variables may have changed

class App extends PureComponent {
  cellRender = (value, index, record) = > {
    return record.name + this.name;
  };
  render() {
    return <List cellRender={this.cellRender} />; }}Copy the code

Object to hijack

Local updates are made by implementing observation objects in a manner similar to [email protected] and Mobx. This approach requires users to avoid using setState methods when using it.

@inject("color")
@observer
class Btn extends React.Component {
  render() {
    return (
      <button style={{ color: this.props.color}} >{this.props.text}</button>
    );
  }
}

<Provider color="red">
  <MessageList>
    <Btn />
  </MessageList>
</Provider>;
Copy the code

For this example, only Button will be rerendered when color changes.

In fact, for 80% of the cases, the above three methods have met the performance optimization of these scenarios, but the above mentioned optimization is in the application layer, in fact, for the developer to put forward certain requirements, what is the way to carry out some optimization at the bottom?

RequestIdleCallback

Thankfully, the browser has a requestIdleCallback API, which allows the browser to execute scripts when it is idle, in the following ways:

requestIdleCallback((deadline) = > {
  if (deadline.timeRemaining() > 0) {}else{ requestIdleCallback(otherTasks); }});Copy the code

The main point of the above example is that if the browser runs out of idle time in the current frame, it will initiate another idle period call. (Note: Around 2018, Facebook abandoned the native API of requestIdleCallback, discussed)

Previously we said that React diff calculations take a lot of time, so let’s think about whether we can solve the experience problem by implementing diff calculations in React. The answer is yes, but there are several problems:

  1. Because each idle time is limited, the program is required to keep the current state when performing diff and wait for the next idle time to call again. This is interruptible, recoverable.
  2. Programs need to have a sense of priority. To put it simply, it is necessary to mark which tasks are of high priority and which tasks are of low priority, so as to have the basis of scheduling. React Fiber is a prioritized scheduling strategy. Look at the above two questions, the most important part is actually can interrupt and restore, how to achieve interrupt and restore?

Fiber of Fibonacci sequence

Here’s how you can rewrite Fibonacci numbers using Fiber. In computer science, there’s a saying that “any recursive program can be implemented using loops.” In order for a program to be interruptible, a recursive program must be rewritten into a loop.

Recursive Fibonacci sequence:

function fib(n) {
  if (n <= 2) {
    return 1;
  } else {
    return fib(n - 1) + fib(n - 2); }}Copy the code

If we use Fiber as a loop, we need to expand the program and keep the intermediate state of execution. The intermediate state is defined as the structure below, although this example is not equivalent to React Fiber.

function fib(n) {
  let fiber = { arg: n, returnAddr: null.a: 0 };
  // mark the loop
  rec: while (true) {
    // When the expansion is complete, start the calculation
    if (fiber.arg <= 2) {
      let sum = 1;
      // Find the parent
      while (fiber.returnAddr) {
        fiber = fiber.returnAddr;
        if (fiber.a === 0) {
          fiber.a = sum;
          fiber = { arg: fiber.arg - 2.returnAddr: fiber, a: 0 };
          continue rec;
        }
        sum += fiber.a;
      }
      return sum;
    } else {
      / / a first
      fiber = { arg: fiber.arg - 1.returnAddr: fiber, a: 0}; }}}Copy the code

In fact, React Fiber is inspired by the structure above. As you can see, the structure of the React Fiber is similar to the structure below, which is very similar to the stack of program execution. When the input parameter of fiber is less than 2, the parent is continuously searched until there is no parent node, and finally the sum value is obtained.

On the left is an expanded structure and on the right is a schematic of the call stack stacked upwards

So Fiber takes up more memory and execution performance than Stack. This example is a little bit more intuitive. But why does the React Fiber approach improve JS performance? There are other optimizations involved, such as no need to work with older browsers, reduced code, etc.

React Fiber structure

Now let’s take a look at the structure of a Fiber Node, as shown in the figure below, a very typical linked list structure, which is actually inspired by the stack expansion above, with a lot more properties than version 15.

{
  tag, // Mark special component types such as fragments, ContextProvider, etc
  type, // The actual description of the component's node, such as div, Button, etc
  key, // The key is the same as 15. If the key is the same, the node can be reused next time
  child, // The node's child
  sibling, // Sibling of the node
  return.// Is actually the parent node of this node
  pendingProps, // Start by setting the pendingProps
  memoizedProps, // Set memoizedProps at the end, and if they are the same, reuse the previous stateNode
  pendingWorkPriority, // Priority of the current node,
  stateNode, // Instance of the component associated with the current node
  effectTag // Mark the type of fiber that needs to be operated on, such as delete, update, etc. }Copy the code

We can traverse the root of a Fiber Node using the same method as the Fibonacci sequence above, which is actually a relatively simple linked list traversal method.

Fiber’s descendant, the Custom Renderer

In the process of implementing Fiber, in order to better realize the need for scalability, we derived the React Reconciler independent package, through which we could customize a Custom Renderer. It defines a set of standardized interfaces that allow us to drive the host environment through the virtual DOM without worrying about how Fiber works internally.

A more complete example exploring the Custom Renderer

Start the way

With the following standardized Custom Renderer startup code, we only need to implement part of HostConfig to use the React Reconclier scheduling capabilities:

import Reconciler from 'react-reconclier';

const HostConfig = {};
const CustomRenderer = Reconciler(HostConfig)
let root;
const render = function(children, container) {
    if(! root) { root = CustomRenderer.createContainer(container); } CustomRenderer.updateContainer(children, root); } render(<App/>, doucment.querySelector('#root')
Copy the code

The core HostConfig method is createInstance, which creates an instance of type TYPE. If the host environment is Web, you can call createElement directly

createInstance(type,props,rootContainerInstance,hostContext) {
   / / conversion props
   return document.createElement(
      type,
      props,
    );
 }
Copy the code

Across the server-side implementation

Derivative, now cross-end solutions, basically this kind of runtime solutions can use the CustomRenderer idea, to achieve multiple code. As a simple example, suppose I write the following code

function App() {
  return <Button />;
}
Copy the code

A Button can use an interception in createInstance, or implement a different Renderer for different ends. Here’s a pseudocode

Mobile Renderer

import { MobileButton } from 'xxx';

createInstance(type,props,rootContainerInstance,hostContext) {
   const components = {
   	Button: MobileButton
   }
   return new components[type](props) / / pseudo code
 }
Copy the code

API design issues

While the CustomRenderer looks good, there are actually some compromises in the overall API design for the Web. For example, the methods shouldSetTextContent and createTextInstance are designed for text alone. Basically, because the Web operates on some element text, there is no way to use a uniform document.createElement. You must use Document.CreateTextNode, but in many other rendering scenarios you don’t need to implement these methods separately or return false directly

The React DOM implementation

export function shouldSetTextContent(type: string, props: Props) :boolean {
  return (
    type === 'textarea' ||
    type === 'option' ||
    type === 'noscript' ||
    typeof props.children === 'string' ||
    typeof props.children === 'number'| | -typeof props.dangerouslySetInnerHTML === 'object'&& props.dangerouslySetInnerHTML ! = =null&& props.dangerouslySetInnerHTML.__html ! =null)); }Copy the code

Some other renderers

export function shouldSetTextContent() {
  return false;
}
Copy the code

summary

In this article, we will explore the problems React Fiber is trying to solve, including the inspiration of Fiber architecture and the application of Custom Renderer after implementing Fiber architecture. Hopefully more scenarios can take advantage of the Custom Renderer capabilities, and here are some common Custom renderers in the community. Finally, this article only represents personal views, if any mistakes welcome criticism and correction.

The resources

ReactFiber

CallStack

requestIdleCallback

React Reconclier

This article is published from netease Cloud Music big front end team, the article is prohibited to be reproduced in any form without authorization. Grp.music – Fe (at) Corp.Netease.com We recruit front-end, iOS and Android all year long. If you are ready to change your job and you like cloud music, join us!