banner
他山之石

他山之石

Write high-performance React code.

This article summarizes some suggestions for writing React code that helps understand React's rendering mechanism, mainly by reducing unnecessary re-renders to improve React performance.

image

Looking back on my experience with React development, it has been almost two years. I have always wanted to write an article to introduce how to write high-performance React code or performance optimization. So, I have summarized some practices for writing high-performance React code, which may deepen your understanding of React.

Main Factors Affecting React Performance#

React focuses on building user interfaces and internally updates views through Virtual DOM and Diff algorithms, which gives it high performance. When the state and props of a component change, React will re-render and update the view. During the render process, React converts JSX syntax into virtual DOM and performs a Diff algorithm to compare the changes between the new and old DOM, thus updating the view partially. Now, imagine that after render is executed, JSX is converted to virtual DOM, then Diff is performed, and finally it is found that the new and old DOM are the same, so the view is not updated. This leads to unnecessary rendering, resulting in performance waste. Therefore, reducing unnecessary rendering becomes the main factor in optimizing React performance.

When writing React components, you will definitely use variables, which can be stored in this, stored in state, or used as global variables. So when should you use state? The answer is that only variables related to the view should be stored in state, while variables unrelated to the view can be stored in this. If variables unrelated to view updates are stored in state, and this variable needs to be updated frequently, and each update requires re-rendering, it will inevitably cause performance waste. Therefore, using this to store such variables is a better choice.

shouldComponentUpdate#

Consider the following code:

export default class Home extends Component {
  state = {
    count: 0
  }

  add = () => {
    this.setState({count: 0});
  }
  
  render() {
    console.log('render');
    return (
      <div className="App">
        <div onClick={this.add}>+++</div>
        <p className="App-intro">
          {this.state.count}
        </p>
      </div>
    );
  }
}

When clicking +++ to trigger the add event, setState updates the state, but the new and old states are exactly the same. React does not optimize in the Component component, resulting in unnecessary rendering. In this case, we need to optimize manually. When the state or props of a react component are updated, the lifecycle method shouldComponentUpdate is triggered. In shouldComponentUpdate, we can compare the new and old states and props to determine whether the component needs to be re-rendered.

shouldComponentUpdate(nextProps, nextState) {
    if(nextState.count === this.state.count) {
      return false;
    }
}

By default, shouldComponentUpdate returns true, which means it always needs to be re-rendered. In this case, we use shouldComponentUpdate to compare and return false if the new and old states are the same, so React will not re-render.

When the state is a string or other basic type, shouldComponentUpdate can handle it well. But what if the state is an object or an array, which are reference types? For reference types, only recursive comparison can determine whether they are equal, or you can use JSON.stringify(nextState.obj) === JSON.stringify(this.state.obj) to compare. However, when the object nesting level is deep or when it contains functions, regular expressions, and other types, shouldComponentUpdate loses its usefulness.

PureComponent#

React.PureComponent is almost identical to React.Component, but React.PureComponent implements shouldComponentUpdate() by shallowly comparing the prop and state. In other words, PureComponent internally implements shouldComponentUpdate and we don't need to write comparison code anymore. React.PureComponent's shouldComponentUpdate() only performs shallow comparison on objects. If the object contains complex data structures, it may produce incorrect negative judgments due to inconsistent deep data (resulting in the view not being updated).

Tests have shown that using PureComponent performs slightly better than using shouldComponentUpdate.

Stateless Component#

Stateless components, also known as functional components.

const Text = ({ children = 'Hello World!' }) =>
  <p>{children}</p>

Functional components do not have a class declaration, lifecycle methods, or component instantiation process. Therefore, they do not need to allocate extra memory, which improves rendering performance.

Arrow Functions and Bind#

When binding event handlers to components, we usually use bind or arrow functions to ensure that the state can be accessed correctly.

<Button onClick={this.update.bind(this)} />

Or

<Button onClick={() => { console.log("Click"); }} />

In the above cases, bind returns a completely new function each time, and arrow functions are reassigned every time render is called (similar to using bind). For the component, a new function is bound each time, and the Button component will consider the props before and after as different, causing re-rendering. There are two recommended ways to write this:

update = () => {
    //
}
<Button onClick={this.update} />

Or

constructor(props) {
    super(props);
    this.update = this.update.bind(this);
}

update() {
    //
}
<Button onClick={this.update} />

Inline Styles#

When setting styles for components, you may write code like this:

<Button style={{width: 100, marginTop: 50 }} />

With this approach, the style returned each time render is called is a completely new object. We know that reference types are compared by reference address, and the results are always unequal each time, which also leads to re-rendering. The recommended approach is to use className instead.

<Button className="btn" />

Property Passing and []#

When setting default values for components:

<RadioGroup options={this.props.options || []} />

If this.props.options is always null, it means that each time [] is passed to <RadioGroup />, but literal arrays and new Array() have the same effect, always generating new instances. Therefore, although it seems that the same empty array is passed to the component each time, for the component, it is always a new property and will cause rendering. The correct way is to save some commonly used values as variables:

const DEFAULT_OPTIONS = [];

<RadioGroup options={this.props.options || DEFAULT_OPTIONS} />
Note: `DEFAULT_OPTIONS` should not be written in `render`. When written in `render`, a new array will still be generated each time, and re-rendering will still occur. The correct approach is to place it outside the component or use `this.DEFAULT_OPTIONS = []`.

map and key#

In React, you often use map to render a list. When you forget to set the key attribute for each list element, React will issue a warning in the console: "a key should be provided for list items". In fact, keys can help React identify which elements have changed when certain elements are added or removed from the DOM. Therefore, you should assign a unique identifier to each element in the array.

The key of an element should ideally be a unique string that the element has in the list. Usually, we use the id from the data as the key for the element:

const todoItems = todos.map((todo) =>
  <li key={todo.id}>
    {todo.text}
  </li>
);

When an element does not have a unique id, you can use its index as the key:

const todoItems = todos.map((todo, index) =>
  // Only do this if items have no stable IDs
  <li key={index}>
    {todo.text}
  </li>
);

When the data in todoItems changes, the list with the index as the key will be re-rendered. Therefore, it is not recommended to use the index for sorting, as it will slow down rendering.

Reduce Variable Execution in render#

As we already know, when the state and props of a component change, render will be executed. Therefore, when there are variable or function executions in render, it may affect rendering performance. Therefore, it is recommended to reduce variable or function executions in render.

Simplify Props and State#

From what we have learned above, when the props and state are reference types, React only performs shallow comparison internally. Therefore, it is recommended to set props and state as basic data types, avoid deep nesting of objects, reduce the number of props parameters, and flatten the props and state objects of the component as much as possible. All of these can optimize the rendering performance of React to some extent.

Conclusion#

This article discusses a series of principles for writing React code, aiming to improve React code performance by reducing unnecessary re-renders. Performance optimization has always been a persistent topic, and there are many other aspects to consider, such as using immutable data structures with immutable.js, using reselect to cache complex calculations in Redux, and splitting components into smaller ones. These are all topics worth exploring. Until next time.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.