DEV Community

Cover image for Getting those performance gains in React
Tushar Kashyap
Tushar Kashyap

Posted on • Edited on

Getting those performance gains in React

This article will be updated with useMemo and useCallback very soon.

This article is aimed to tell the various ways you can boost the performance of your React application 💪.

React itself is quite fast and all the re-rendering is not a problem in the majority of cases. My suggestion would be to read this article and not apply anything until you actually get a performance problem. Bookmark it for later reference when you do apply any of these features/techniques.

1. Using shouldComponentUpdate()

shouldComponentUpdate() is an update life cycle method which can only be used inside class components.

Usage

Inside the method we can return true if we want the component to update or false if we do not want the component to update. To help deciding whether to return true or false (i.e. to update or not) shouldComponentUpdate() takes in two parameters (nextProps and nextState). We can use the incoming props/state and the current props/state to create some meaningful logic that will only return true when it actually makes a difference. We will see this in an example later.

The normal behavior is every time there is an update to the component (i.e. changed state or props) the render() method will be called and the JSX will be re-rendered even if that change makes no difference at all. So that means using no shouldComponentUpdate() defaults to using shouldComponentUpdate() and returning true on every update.

Example

class ChangeTextColor extends React.Component {
  state = {
    textColor: 'blue'
  };

  handleColorChange = (e) => {
    const {name} = e.target;
    this.setState({textColor: name});
  };

  shouldComponentUpdate() {
    return true;  // default behavior, tells component to update every time
  }

  render() {
    console.log("re-rendering...");
    return (
      <>
        <h1 style={{ color: this.state.textColor }}>Unicorn</h1>
        <input
          type="button"
          value="Blue"
          name="blue"
          onClick={this.handleColorChange}
        />
        <input
          type="button"
          value="Red"
          name="red"
          onClick={this.handleColorChange}
        />
        <input
          type="button"
          value="yellow"
          name="yellow"
          onClick={this.handleColorChange}
        />
      </>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

The above examples has a text whose text-color can be changed by clicking on the different buttons. Now, if you check the console every time you change the color you will see, re-rendering... logged out.

But even if you press the same button as the text color you will see re-rendering... every time. That means this component is re-rendered every time even if the state is not changing meaningfully. Let's edit the shouldComponentUpdate() method to improve this component.

shouldComponentUpdate(nextProps, nextState) {
    if (nextState.textColor !== this.state.textColor) {   // when there is a meaningful change
      return true;
    } else return false;
  }
Enter fullscreen mode Exit fullscreen mode

Now when you try to click a button that does not change the color of the text, the re-rendering will not take place (as we have returned false in shouldComponentUpdate). You can take a look at the console to confirm the same as only when the text-color changes re-rendering... is logged out.

Here's the sample code for you to play around with - shouldComponentUpdate() CodeSandbox

2. Using Pure Components

Pure Components are components which prevent useless re-rendering by checking if the updated props/state value is the same as current props/state value. But there's a catch which we will discuss shortly.

Usage

Pure Components are fairly simple to use. By just extending React.PureComponent instead of React.Component you can convert a class component into a Pure Component. Now when the state/props are changed by any means the component will not blindly re-render every time like a regular component does (i.e. it will do a shallow comparison before before re-rendering).

Example

We will use the same example as before but this time we don't need to write the shouldComponentUpdate() method because a pure component is smart enough to not re-render blindly.

class ChangeTextColor extends React.PureComponent {
  state = {
    textColor: "blue"
  };

  handleColorChange = e => {
    const { name } = e.target;
    this.setState({ textColor: name });
  };

  render() {
    console.log("re-rendering...");
    return (
      <>
        <h1 style={{ color: this.state.textColor }}>Unicorn</h1>
        <input
          type="button"
          value="Blue"
          name="blue"
          onClick={this.handleColorChange}
        />
        <input
          type="button"
          value="Red"
          name="red"
          onClick={this.handleColorChange}
        />
        <input
          type="button"
          value="yellow"
          name="yellow"
          onClick={this.handleColorChange}
        />
      </>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

What is a Shallow Comparison?

In JavaScript there are primitives and then there are objects (reference type data), primitives are equal when the value and the type (number, string...) are same.

let a = "unicorn";
let b = "unicorn";

console.log(a === b);   // true
Enter fullscreen mode Exit fullscreen mode

Two objects are equal only when they both are referencing to the same object in memory.

let a = {a:1, b:2};
let b = {a:1, b:2};

console.log(a === b);   // false

let c = a;

console.log(a === c)   // true
Enter fullscreen mode Exit fullscreen mode

This must be obvious if you have been working with JavaScript for some time, but when it comes to pure components this behavior can be harmful. Let's look how

Now, imagine you have an array (an object) in your state. At some point in your code if something is pushed inside that array, pure component will think nothing has changed because it still refers to the same array and it will not re-render.

The key point here is do not ever mutate an object/array directly because it still points to the same reference. Always return new object/array.

Another thing to keep in mind is if a parent component is pure and does not re-render all of it's children will also not re-render, so a good practice is to have pure components as children to a parent pure component.

3. Using React.memo()

The things you can do with Pure Components in class components are somewhat possible in functional components using React.memo(), it's a higher order component (HOC), i.e. it wraps around your component and provides you with a new component.

Usage

const MyComponent = React.memo(function MyComponent(props) {
  /* render using props */
});
Enter fullscreen mode Exit fullscreen mode

On a higher level just keep in mind that when you wrap your component with React.memo it will not re-render blindly and will shallowly compare the props to decide. A thing to keep in mind is it will only check for props, if the component wrapped in React.memo has a useState or useContext Hook in its implementation, it will still re-render when state or context change.

Again it works great with primitive data types like string and numbers but runs into issues with objects and functions (which are also objects).

Example

First, using primitives.

const Text = React.memo(function Text(props) {
  console.log("re-rendering...") 
  return <h1 style={{color: props.color}}>Unicorn</h1>
})

class ColorChanger extends React.Component {
state = {
  textColor: "blue"
}

handleColorChange = (e) => {
  const {name} = e.target;
  this.setState({textColor: name});
};

render() {
    return (
      <div>
        <Text color={this.state.textColor} />

        <input
          type="button"
          value="Blue"
          name="blue"
          onClick={this.handleColorChange}
        />
        <input
          type="button"
          value="Red"
          name="red"
          onClick={this.handleColorChange}
        />
        <input
          type="button"
          value="yellow"
          name="yellow"
          onClick={this.handleColorChange}
        />
      </div>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

This is more or less the same example as before using React.memo(), here also no useless re-rendering... will be logged out and will only re-render when color changes actually (as the prop passed to Text changes).

In the last example we will see what happens when we pass a function as a prop.

I will write the same example as above with the slightest of changes

const Text = React.memo(function Text(props) {
  console.log("re-rendering...")
  return <h1 style={{color: props.color}}>Unicorn</h1>
})

class ColorChanger extends React.Component {
state = {
  textColor: "blue"
}

handleColorChange = (e) => {
  const {name} = e.target;
  this.setState({textColor: name});
};

render() {
    return (
      <div>
        <Text color={this.state.textColor} randomFunction={() => console.log("useless function")} />

        <input
          type="button"
          value="Blue"
          name="blue"
          onClick={this.handleColorChange}
        />
        <input
          type="button"
          value="Red"
          name="red"
          onClick={this.handleColorChange}
        />
        <input
          type="button"
          value="yellow"
          name="yellow"
          onClick={this.handleColorChange}
        />
      </div>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Here I just passed a randomFunction prop which I'm not even using but now when I press the same button the Text component is re-rendering. What is happening here?
The reason our memoization breaks here is because it's creating a new function on every re-render. And we learned in shallow comparison that any complex type pointing to a different reference are not equal even if it's definition is exactly the same.

Just play around with the code and keep an eye on the console, try removing the function prop and then check the console.
React.memo() CodeSandbox

There are ways you can overcome this (like the useCallback hook) but it will be a topic for another day. Just remember, it works great with primitives but you have to do some extra work to make it work with complex types.

Thank you for reading this article 😄

My suggestion will be to keep writing your react code freely with an open mind and learn to use the profiler to catch performance problems if you feel it's getting slow. These are 3 ways out of the many ways you can improve performance in React Applications.

Also, whenever I had to think of random text only Unicorn seemed to cross my mind. So that's that.
Unicorn gif

Top comments (0)