• Performance Optimisations for React Applications
  • The Nuggets translation Project
  • Translator: woota
  • Proofreader: Malcolm U, Zheaoli

The point outline

The main performance issues with React applications are redundant processing and DOM alignment of components. To avoid these performance traps, you should return false in shouldComponentUpdate whenever possible.

In short, it comes down to the following two points:

  1. To speed up shouldComponentUpdateThe check
  2. To simplify the shouldComponentUpdateThe check

Disclaimer!

The example in this article is written with React + Redux. If you are using another data flow library, the principles are the same but the implementation will be different.

I didn’t use the Immutability library in this article, just some plain ES6 and a bit of ES7. Some things are a little easier with immutable databases, but I’m not going to cover that here.

What are the main performance issues with React applications?

  1. Redundant operations in components that do not update the DOM
  2. The DOM compares leaf nodes that do not need to be updated

    • While DOM alignment is great and speeds up React, the computational cost is significant

How does React default render behavior?

Let’s look at how React renders components.

Initialize render

When initializing the render, we need to render the entire application (green = rendered nodes)

Every node is rendered — it’s awesome! Now our application represents our initial state.

Put forward the change

We want to update some of the data. These changes are associated with only one leaf node

Ideal update

We only want to render these nodes along the critical path to the leaf node

The default behavior

If you don’t tell React not to do this, it will (orange = wasteful rendering)

Oh, no! All of our nodes have been re-rendered.

Every component of React has a shouldComponentUpdate(nextProps, nextState) function. Its job is to return true when the component needs to be updated and false when the component does not need to be updated. Returning false causes the component’s render function not to be called. React always returns true in shouldComponentUpdate by default, even if you don’t explicitly define a shouldComponentUpdate function.

// Default behavior
shouldComponentUpdate(nextProps, nextState) {
    return true;
}Copy the code

This means that by default, every time you update your props at the top level, every component of the entire application is rendered. This is a major performance issue.

How do we get the desired updates?

Return false in shouldComponentUpdate if possible.

In short:

  1. To speed up shouldComponentUpdateThe check
  2. To simplify the shouldComponentUpdateThe check

Accelerate shouldComponentUpdate check

Ideally we don’t want to do deep checking in shouldComponentUpdate as this is very expensive, especially if you have large scale and large data structures.

class Item extends React.component {
    shouldComponentUpdate(nextProps) {
      // It's expensive
      return isDeepEqual(this.props, nextProps);
    }
    // ...
}Copy the code

An alternative is to change the object’s reference whenever its value changes.

constnewValue = { ... oldValue// Make the changes you want here
};

// Quick check -- just check the references
newValue === oldValue; // false

// You can also use object. assign if you wish
const newValue2 = Object.assign({}, oldValue);

newValue2 === oldValue; // falseCopy the code

Use this technique in the Redux Reducer:

In this Reducer we will change the description of an item
export default (state, action) {

    if(action.type === 'ITEM_DESCRIPTION_UPDATE') {

        const { itemId, description } = action;

        const items = state.items.map(item => {
            // Action has nothing to do with this item -- we can return this item without modification
            if(item.id ! == itemId) {return item;
            }

            // We want to change this item
            // This preserves the original item value, but
            // A new object with updated description is returned
            return {
              ...item,
              description
            };
        });

        return {
          ...state,
          items
        };
    }

    return state;
}Copy the code

If you do this, you just need to check for references in the shouldComponentUpdate function

// Super fast -- all you do is check references!
shouldComponentUpdate(nextProps) {
    return isObjectEqual(this.props, nextProps);
}Copy the code

An example implementation of isObjectEqual

const isObjectEqual = (obj1, obj2) => {
    if(! isObject(obj1) || ! isObject(obj2)) {return false;
    }

    // Whether references are the same
    if(obj1 === obj2) {
        return true;
    }

    // Do they contain the same key names?
    const item1Keys = Object.keys(obj1).sort();
    const item2Keys = Object.keys(obj2).sort();

    if(! isArrayEqual(item1Keys, item2Keys)) {return false;
    }

    // Does every object corresponding to the attribute have the same reference?
    return item2Keys.every(key => {
        const value = obj1[key];
        const nextValue = obj2[key];

        if(value === nextValue) {
            return true;
        }

        // Check the depth of one level
        return Array.isArray(value) && 
            Array.isArray(nextValue) && 
            isArrayEqual(value, nextValue);
    });
};

const isArrayEqual = (array1 = [], array2 = []) => {
    if(array1 === array2) {
        return true;
    }

    // Check a level of depth
    return array1.length === array2.length &&
        array1.every((item, index) => item === array2[index]);
};Copy the code

Simplify shouldComponentUpdate check

Let’s start with a complex shouldComponentUpdate example

// Focus on separate data structures (standardized data)
const state = {
    items: [
        {
            id: 5,
            description: 'some really cool item'}]// Represents the object that the user interacts with the system
    interaction: {
        selectedId: 5}};Copy the code

If you organize your data this way, it makes checking in shouldComponentUpdate difficult

import React, { Component, PropTypes } from 'react'

class List extends Component {

    propTypes = {
        items: PropTypes.array.isRequired,
        iteraction: PropTypes.object.isRequired
    }

    shouldComponentUpdate (nextProps) {
        // Does the element in items change?
        if(! isArrayEqual(this.props.items, nextProps.items)) {
            return true;
        }

        // Things can get scary from here

        // If interaction does not change, it can return false (good!).
        if(isObjectEqual(this.props.interaction, nextProps.interaction)) {
            return false;
        }

        // If the code runs here, we know:
        // 1. Items do not change
        // How much interaction do you have
        // We need to see if the interaction changes are relevant to us

        const wasItemSelected = this.props.items.any(item => {
            return item.id === this.props.interaction.selectedId
        })
        const isItemSelected = nextProps.items.any(item => {
            return item.id === nextProps.interaction.selectedId
        })

        // Return true if it has changed
        // Return false if no change has occurred
        returnwasItemSelected ! == isItemSelected; } render() {<div>
            {this.props.items.map(item => {
                const isSelected = this.props.interaction.selectedId === item.id;
                return (<Item item={item} isSelected={isSelected} />);
            })}
        </div>}}Copy the code

Question 1:shouldComponentUpdatebulky

You can see that a shouldComponentUpdate corresponding to a very simple data is huge and complex. This is because it needs to know the structure of the data and how they relate to each other. The shouldComponentUpdate function only grows in complexity and size with your data structure. This can easily lead to two mistakes:

  1. Return false when it should not (application displays error status)
  2. Return true when it should not (causing performance problems)

Why make things so complicated? You just want to make these tests so easy that you don’t have to think about them at all.

Problem 2: Strong coupling between parent and child levels

In general, applications promote loose coupling (the less one component knows about the other, the better). The parent component should try to avoid knowing how its children work. This allows you to change the behavior of the child components without letting the parent know about the changes (assuming the PropsTypes remain unchanged). It also allows child components to operate independently without having their parent tightly control their behavior.

Solutions:Flatten your data

By flattening (merging) your data structures, you can reuse very simple reference checks to see if anything has changed.

const state = {
    items: [
        {
            id: 5,
            description: 'some really cool item'.// Interaction now exists inside item
            interaction: {
                isSelected: true}}}};Copy the code

Organizing your data this way makes it easy to check in shouldComponentUpdate

import React, {Component, PropTypes} from 'react'

class List extends Component {

    propTypes = {
        items: PropTypes.array.isRequired
    }

    shouldComponentUpdate(nextProps) {
        // So easy, Mom no longer need to worry about my update check
        return isObjectEqual(this.props, nextProps);
    }

    render() {
        <div>
            {this.props.items.map(item => {

                return (
                <Item item={item}
                    isSelected={item.interaction.isSelected} />)})}</div>}}Copy the code

If you want to update interaction you change the entire object reference

// redux reducer
export default (state, action) => {

    if(action.type === 'ITEM_SELECT') {

        const { itemId } = action;

        const items = state.items.map(item => {
            if(item.id ! == itemId) {return item;
            }

            // Change the reference to the entire object
            return {
                ...item,
                interaction: {
                    isSelected: true}}})return {
            ...state,
            items
        };
    }

    return state;
};Copy the code

Error: Reference checking with dynamic props

An example of creating dynamic props

class Foo extends React.Component {
    render() {
        const {items} = this.props;

        // This object has a new reference every time
        const newData = { hello: 'world' };


        return <Item name={name} data={newData} />}} class Item extends React.Component {// Check always returns true even if two objects have the same value, ShouldComponentUpdate (nextProps) {return isObjectEqual(this. Props, nextProps); }}Copy the code

Usually we don’t create a new props in the component to pass it down. However, this is more common in loops

class List exntends React.Component {
    render() {
        const {items} = this.props;

        <div>Const newData = {hello: 'world', isFirst: index === 0}; {items.map((item, index) => {// This object gets a new reference every time return<Item name={name} data={newData} />
            })}
        </div>}}Copy the code

This is common when creating functions

import myActionCreator from './my-action-creator';

class List extends React.Component {
    render() {
        const {items, dispatch} = this.props;

        <div>{items.map(item => {// The reference to this function becomes const callback = () => {dispatch(myActionCreator(item)); } return<Item name={name} onUpdate={callback} />
            })}
        </div>}}Copy the code

Problem-solving strategies

  1. Avoid creating dynamic props in components

Improve your data model so that you can pass down props directly

  1. Pass the dynamic props to a type that satisfies congruence (===)

eg:

– boolean

– number

– string

const bool1 = true;
const bool2 = true;

bool1 === bool2; // true

const string1 = 'hello';
const string2 = 'hello';

string1 === string2; // trueCopy the code

If you really need to pass a dynamic object, pass it as a string and deconstruct it at the child level

render() {
    const {items} = this.props;

    <div>Const bad = {id: item.id, type: item.type}; / / the same value can meet the strict congruent '= = =' const good = ` ${item. Id} : : ${item. Type} `; return<Item identifier={good} />
        })}
    </div>
}Copy the code

Special case: function

  1. Avoid transfer functions if you can. Instead, dispatch the child component freely. This has the added benefit of moving business logic out of the component.
  2. Ignore function checking in shouldComponetUpdate. That’s not ideal, because we don’t know if the value of the function has changed.
  3. Create an immutable binding for data -> function. You can save them to the state in componentWillReceiveProps functions. This way you don’t get a new reference every time you render. This method is extremely cumbersome because you need to maintain and update a list of functions.
  4. Create an intermediate component with the correct this binding. This is also not ideal because you introduce a layer of redundancy into the hierarchy.
  5. Any other method you can think of that avoids creating a new function every time render is called.

An example of scenario 4

// Add another layer of 'ListItem'
<List>
    <ListItem>// You can create the correct this binding here<Item />
    </ListItem>
</List>Class ListItem extends React.Component {// Always get the correct this binding because it is bound to the instance // Thanks to ES7! const callback = () => { dispatch(doSomething()); } render() { return<Item callback={this.callback} item={this.props.item} />}}Copy the code

tool

All of the rules and techniques listed above were discovered using performance measurement tools. Using tools can help you identify specific performance issues in your application.

console.time

This one is pretty simple:

  1. Start a timer
  2. Do something
  3. Stop timer

A good practice is to use Redux middleware:

export default store => next => action => {
    console.time(action.type)

    // 'next' is a function that takes 'action' and sends it to 'reducers' for processing
    // This will result in the first rendering you should have
    const result = next(action);

    // How long did the rendering take?
    console.timeEnd(action.type);

    return result;
};Copy the code

Use this method to track every action you apply and the time it takes to render it. You can quickly know which actions take the longest to render, so you can start from there when you solve performance problems. Getting a time value can also help you determine if the performance tuning you’ve done is working.

React.perf

This tool works the same as console.time, but uses the React performance tool:

  1. Perf.start()
  2. do stuff
  3. Perf.stop()

Examples of Redux middleware:

import Perf from 'react-addons-perf';

export default store => next => action => {
    const key = `performance:${action.type}`;
    Perf.start();

    // Get the new state rendering application
    const result = next(action);
    Perf.stop();

    console.group(key);
    console.info('wasted');
    Perf.printWasted();
    // You can print any Perf measurements you are interested in here

    console.groupEnd(key);
    return result;
};Copy the code

Similar to the console.time method, it lets you see the performance metrics for each of your actions. More information about React performance addon can be found here

Browser tools

The CPU Analyzer flame chart also comes in handy when looking for performance problems with your application.

When doing performance analysis, the flame chart shows the state of your code’s Javascript stack every millisecond. As you record, you know exactly which function was executed at any point in time, how long it was executed, and who called it. – Mozilla

Firefox: Click to view

Chrome: Click to see

Thanks for reading, and good luck building a high-performance React app!