This article summarizes optimization tips for React App from the perspective of the Render function. Note that the React 16.8.2 version is covered (which is Hooks), so at least know useState to eat it.

Begin the text.


When discussing performance issues with React App, component rendering speed is an important issue. Before diving into specific optimization recommendations, here are three things to understand:

  1. What are we saying when we say “render”?
  2. When will “render” be executed?
  3. What happens during the render process?

Interpreting render function

This part deals with the concepts of Reconciliation and Diffing, and of course the official documentation is here.

What are we saying when we say “render”?

Anyone who has written React will know this.

In the class component, we refer to the Render method:

class Foo extends React.Component { render() { return <h1> Foo </h1>; }}Copy the code

In functional components, we refer to the functional component itself:

function Foo() {
  return <h1> Foo </h1>;
}Copy the code

When will “render” be executed?

The render function is called in two scenarios:

1. Status update

A. Update the status of the class component inherited from react.component. b. Update the status of the class component inherited from react.component. c
import React from "react";
import ReactDOM from "react-dom";

class App extends React.Component {
  render() {
    return <Foo />;
  }
}

class Foo extends React.Component {
  state = { count: 0 };

  increment = () => {
    const { count } = this.state;

    const newCount = count < 10 ? count + 1 : count;

    this.setState({ count: newCount });
  };

  render() {
    const { count } = this.state;
    console.log("Foo render");

    return (
      <div>
        <h1> {count} </h1>
        <button onClick={this.increment}>Increment</button>
      </div>
    );
  }
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);Copy the code

As you can see, the logic in the code is that when we click on it it updates count, and when it gets to 10, it stays at 10. Add a console.log so we know if render was called. As you can see from the result, render is still called even if count goes above 10.

Conclusion: Inheriting the class component of React.component. render is triggered when setState is called, even if the state does not change.

B. When a functional component updates its state

We implement the same component with functions, but of course we use useState hook because we want to be stateful:

import React, { useState } from "react";
import ReactDOM from "react-dom";

class App extends React.Component {
  render() {
    return <Foo />;
  }
}

function Foo() {
  const [count, setCount] = useState(0);

  function increment() {
    const newCount = count < 10 ? count + 1 : count;
    setCount(newCount);
  }

  console.log("Foo render");
  
  return (
    <div>
      <h1> {count} </h1>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);Copy the code

Note that the render call stops when the state value stops changing.

Conclusion: For functional components, the render function is invoked only when the state value changes.

2. When the parent container is re-rendered

import React from "react";
import ReactDOM from "react-dom";

class App extends React.Component {
  state = { name: "App" };
  render() {
    return (
      <div className="App">
        <Foo />
        <button onClick={() => this.setState({ name: "App" })}>
          Change name
        </button>
      </div>
    );
  }
}

function Foo() {
  console.log("Foo render");

  return (
    <div>
      <h1> Foo </h1>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);Copy the code

As soon as you click the Change Name button in the App component, it will be rerendered. Also notice that Foo will be re-rendered regardless of what implementation it is.

Summary: Whether the component is a class component inherited from react.componentor a functional component, the component’s render will be called again once the parent container is rerender.

What happens during the render process?

As soon as the Render function is called, two steps are executed in sequence. These two steps are important to understand in order to know how to optimize React App.

Diffing

In this step, React compares the tree returned by the newly called Render function to the old version of the tree, which is necessary for React to decide how to update the DOM. React uses a highly optimized algorithm to perform this step, but there is still some performance overhead.

Reconciliation

React updates the DOM tree based on diffing results. This step also has a lot of performance overhead due to the need to unmount and mount DOM nodes.

Let’s start our Tips

Tip #1: Allocate state carefully to avoid unnecessary render calls

Let’s take the following example, where the App renders two components:

  • CounterLabel, a method that receives the count value and the state count in an INC parent App.
  • List, receives a list of items.
import React, { useState } from "react";
import ReactDOM from "react-dom";

const ITEMS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];

function App() {
  const [count, setCount] = useState(0);
  const [items, setItems] = useState(ITEMS);
  return (
    <div className="App">
      <CounterLabel count={count} increment={() => setCount(count + 1)} />
      <List items={items} />
    </div>
  );
}

function CounterLabel({ count, increment }) {
  return (
    <>
      <h1>{count} </h1>
      <button onClick={increment}> Increment </button>
    </>
  );
}

function List({ items }) {
  console.log("List render");

  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{item} </li>
      ))}
    </ul>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);Copy the code

Executing the code above shows that whenever the state in the parent component App is updated, the CounterLabel and List are updated.

Of course, it is normal to re-render the CounterLabel because the count has changed; In the case of a List, however, an update is completely unnecessary because it renders independent of count. Although React doesn’t actually update the DOM during the Reconciliation phase — it doesn’t change at all — it still performs the Diffing phase to compare the trees before and after, which still has a performance overhead.

Remember the Diffing and Reconciliation phases of the Render implementation? I’m going to run into what I said before.

Therefore, to avoid unnecessary Diffing overhead, we should consider placing specific state values in lower levels or components (as opposed to the concept of “lifting” in React). In this example, we can solve this problem by managing count in the CounterLabel component.

Tip #2: Merge status updates

Since each status update triggers a new Render call, fewer status updates result in fewer render calls.

The React Class component has a componentDidUpdate(prevProps, prevState) hook that checks whether the props or state has changed. Although it is sometimes necessary to trigger a state update when the props changes, it is always possible to avoid doing a state update after a state change:

import React from "react"; import ReactDOM from "react-dom"; function getRange(limit) { let range = []; for (let i = 0; i < limit; i++) { range.push(i); } return range; } class App extends React.Component { state = { numbers: getRange(7), limit: 7 }; handleLimitChange = e => { const limit = e.target.value; const limitChanged = limit ! == this.state.limit; if (limitChanged) { this.setState({ limit }); }}; componentDidUpdate(prevProps, prevState) { const limitChanged = prevState.limit ! == this.state.limit; if (limitChanged) { this.setState({ numbers: getRange(this.state.limit) }); } } render() { return ( <div> <input onChange={this.handleLimitChange} placeholder="limit" value={this.state.limit} /> {this.state.numbers.map((number, idx) => ( <p key={idx}>{number} </p> ))} </div> ); } } const rootElement = document.getElementById("root"); ReactDOM.render(<App />, rootElement);Copy the code

Here a sequence of range numbers is rendered, ranging from 0 to limit. Whenever the user changes the limit value, we check in componentDidUpdate and set a new list of numbers.

There is no doubt that the above code is adequate, but we can still optimize it.

In the code above, each time the limit changes, we trigger two status updates: the first to modify the limit and the second to modify the list of numbers displayed. In this way, each change in limit causes two render overheads:

// render 1: {limit: 7, numbers: [0, 1, 2, 3, 4, 5, 6] render 1: {limit: 7, numbers: [0, 1, 2, 3, 4, 5, 6] [0, 1, 2, 3, 4, 5, 6] } // render 2: { limit: 4, numbers: [0, 2, 3]Copy the code

Our code logic presents the following problems:

  • We trigger more status updates than we really need;
  • We had “discontinuous” render results, where the list of numbers didn’t match the limit.

To improve, we should avoid changing the list of numbers between status updates. In fact, we can fix it in a status update:

import React from "react";
import ReactDOM from "react-dom";

function getRange(limit) {
  let range = [];

  for (let i = 0; i < limit; i++) {
    range.push(i);
  }

  return range;
}

class App extends React.Component {
  state = {
    numbers: [1, 2, 3, 4, 5, 6],
    limit: 7
  };

  handleLimitChange = e => {
    const limit = e.target.value;
    const limitChanged = limit !== this.state.limit;
    if (limitChanged) {
      this.setState({ limit, numbers: getRange(limit) });
    }
  };

  render() {
    return (
      <div>
        <input
          onChange={this.handleLimitChange}
          placeholder="limit"
          value={this.state.limit}
        />
        {this.state.numbers.map((number, idx) => (
          <p key={idx}>{number} </p>
        ))}
      </div>
    );
  }
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);Copy the code

Tip #3: Use PureComponent and React.memo to avoid unnecessary render calls

We saw in the previous example a way to place a particular state value at a lower level to avoid unnecessary rendering, but this is not always useful.

Let’s look at the following example:

import React, { useState } from "react";
import ReactDOM from "react-dom";

function App() {
  const [isFooVisible, setFooVisibility] = useState(false);

  return (
    <div className="App">
      {isFooVisible ? (
        <Foo hideFoo={() => setFooVisibility(false)} />
      ) : (
        <button onClick={() => setFooVisibility(true)}>Show Foo </button>
      )}
      <Bar name="Bar" />
    </div>
  );
}

function Foo({ hideFoo }) {
  return (
    <>
      <h1>Foo</h1>
      <button onClick={hideFoo}>Hide Foo</button>
    </>
  );
}

function Bar({ name }) {
  return <h1>{name}</h1>;
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);Copy the code

As you can see, Foo and Bar are rerendered whenever the parent App’s state value isFooVisible changes.

This is because in order to determine whether Foo should be rendered or not, we need to maintain isFooVisible in the App, so we can’t strip out the state to lower levels. However, it is still not necessary to re-render the Bar when isFooVisible changes, as the Bar does not rely on isFooVisible. We only want Bar to re-render when the incoming property name changes.

So what are we gonna do about it? Two ways.

First, memoize Bar:

const Bar = React.memo(function Bar({name}) {
  return <h1>{name}</h1>;
});Copy the code

This ensures that the Bar is only re-rendered when the name changes.

In addition, another way is to make the Bar to inherit the React. PureComponent rather than React.Com ponent:

class Bar extends React.PureComponent { render() { return <h1>{name}</h1>; }}Copy the code

Does that sound familiar? We’ve often mentioned that using React.PureComponent provides a performance boost and avoids unnecessary render.

The react. memo wrapped function component inherits the Class component from the React.PureComponent.

Why not have every component inherit PureComponent or use the Memo package?

If this tip allows us to avoid unnecessary re-rendering, why don’t we make every class component PureComponent and wrap every functional component in react.Memo? Why keep React.Com Ponent when there’s a better way? Why don’t functional components be memorized by default?

To be sure, these methods are not always a panacea.

Problems with nested objects

Let’s first consider what the PureComponent and the React.memo components do.

Each time an update is made (including a state update or an overlayer re-rendering), they perform a shallow comparison of the key and value between the new props and state and the old props and state. Shallow comparisons are strict equality checks, and if a difference is detected, Render executes:

ShallowCompare ({name: 'bar'}, {name: 'bar'}); // output: true shallowCompare({ name: 'bar'}, { name: 'bar1'}); // output: falseCopy the code

While comparisons of basic types (strings, numbers, booleans) work fine, complex cases like objects can introduce unexpected behavior:

shallowCompare({ name: {first: 'John', last: 'Schilling'}},
               { name: {first: 'John', last: 'Schilling'}}); // output: falseCopy the code

The references to the objects corresponding to the two names above are different.

Let’s revisit the previous example and modify the props we passed to the Bar:

import React, { useState } from "react";
import ReactDOM from "react-dom";

const Bar = React.memo(function Bar({ name: { first, last } }) {
  console.log("Bar render");

  return (
    <h1>
      {first} {last}
    </h1>
  );
});

function Foo({ hideFoo }) {
  return (
    <>
      <h1>Foo</h1>
      <button onClick={hideFoo}>Hide Foo</button>
    </>
  );
}

function App() {
  const [isFooVisible, setFooVisibility] = useState(false);

  return (
    <div className="App">
      {isFooVisible ? (
        <Foo hideFoo={() => setFooVisibility(false)} />
      ) : (
        <button onClick={() => setFooVisibility(true)}>Show Foo</button>
      )}
      <Bar name={{ first: "John", last: "Schilling" }} />
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);Copy the code

Although the Bar is memorized and the props values have not changed, it is still re-rendered every time the parent component is re-rendered. This is because although the two objects being compared each time have the same value, the references are not the same.

Function props

We can also pass functions as props to components. Of course, in JavaScript functions also pass references, so shallow comparisons are also based on the references they pass.

Therefore, if we pass arrow functions (anonymous functions), the component will still be re-rendered when the parent component is re-rendered.

Tip #4: A better way to write props

One solution to the previous problem is to rewrite our props.

Instead of passing objects as props, we split the objects into primitives:

<Bar firstName="John" lastName="Schilling" />Copy the code

In the case of passing the arrow function, we can always get the same reference by substituting a function that has only been declared once, as follows:

class App extends React.Component{
  constructor(props) {
    this.doSomethingMethod = this.doSomethingMethod.bind(this);    
  }
  doSomethingMethod () { // do something}
  
  render() {
    return <Bar onSomething={this.doSomethingMethod} />
  }
}Copy the code

Tip #5: Control updates

Again, every method has its limits.

The third tip deals with unnecessary updates, but we can’t always use it.

Fourth, in some cases we can’t split objects, and if we pass some sort of nested really complex data structure, it’s hard to split it.

Not only that, but we can’t always pass functions that have only been declared once. In our case, for example, we might not be able to do this if App was a functional component. (In the class component, we could use bind or the in-class arrow function to ensure that this points to and is uniquely declared, which might be problematic in the functional component.)

Fortunately, for both class and functional components, there are ways to control the logic of shallow comparisons.

In the class component, we can use the lifecycle hook shouldComponentUpdate(prevProps, prevState) to return a Boolean and only trigger render when the return value is true.

If we use react. memo, we can pass a comparison function as the second argument.

Attention!The react. memo’s second argument (comparison function) and
shouldComponentUpdateThe logic is reversed; render is triggered only when the return value is false.
Reference documentation.

const Bar = React.memo(
  function Bar({ name: { first, last } }) {
    console.log("update");
    return (
      <h1>
        {first} {last}
      </h1>
    );
  },
  (prevProps, newProps) =>
    prevProps.name.first === newProps.name.first &&
    prevProps.name.last === newProps.name.last
);Copy the code

While this advice is valid, we still need to be careful about the performance overhead of comparing functions. If the props object is too deep, it can consume a lot of performance.

conclusion

The above scenario is still not comprehensive, but it can give some instructive thoughts. Of course, there are many other issues to consider in terms of performance, but following the guidelines above can still lead to a decent performance boost.