Optimizing Performance in React

Performance Tools in React

React-addons-perf

import Perf from 'react-addons-perf'; 
Perf.start() 
// App 
Perf.stop()

The Perf methods can be used to take performance measurements.

  • Perf.printInclusive() prints overall time taken.

  • Perf.printExclusive() prints time excluding mounting time

  • Perf.printWasted() prints time wasted on components that didn't actually render anything.

  • Perf.printOperations() prints all DOM manipulations

  • Perf.getLastMeasurements() prints the measurement from the last Perf session.

Avoid Reconciliation

shouldComponentUpdate()

shouldComponentUpdate(nextProps, nextState) {
 return this.props.color !== nextProps.color;
}

Here we use a shallow comparison to determine if the props of the component have changed. If so, the component should update.

React.PureComponent

This is a React component that implements shouldComponentUpdate() and only diffs and updates when it returns true. Any child of PureComponent must also be a PureComponent.

class CounterButton extends React.PureComponent {
  constructor(props) {
    super(props);
    this.state = {count: 1};
  }
  render() {
    return (
      <button
       color={this.props.color}
       onClick={() => this.setState(state => ({count: state.count + 1}))}>
       Count: {this.state.count}
      </button>
    );
  }
}

Other hooks

useReducer

This is an alternative to useState. Accepts a reducer of type (state, action) => newState, and returns the current state paired with a dispatch method.

Cases to use useReducer over useState:

  1. Complex state logic that involves multiple sub-values

  2. The next state depends on the previous one

  3. Optimize performance for components that trigger deep updates by passing dispatch down instead of callbacks.

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

The dispatch function identity is stable and won't change on re-renders.

useCallback

This returns a memoized callback that only changes if one of the dependencies has changed. This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders (e.g. shouldComponentUpdate).

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

useCallback(fn, deps) is equivalent to useMemo(() => fn, deps).

Not Mutating Data

Code examples

Consider this example:

class ListOfWords extends React.PureComponent {
  render() {
    return <div>{this.props.words.join(',')}</div>;
  }
}

class WordAdder extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      words: ['hello']
    };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    // This section is bad style and causes a bug
    const words = this.state.words;
    words.push('world');
    this.setState({words: words});
  }

  render() {
    return (
      <div>
        <button onClick={this.handleClick} />
        <ListOfWords words={this.state.words} />
      </div>
    );
  }
}

This example creates a bug and "world" is not actually displayed

To solve this problem, we should avoid mutating values that we are using as props or state.

One solution using concat can be:

handleClick() {
  this.setState(state => ({
    words: state.words.concat(['world'])
  }));
}

Or we can use the spread operator in ES6:

handleClick() {
  this.setState(state => ({
    words: [...state.words, 'world'],
  }));
};

For objects, we can also write code to avoid mutating objects. This is the way in which we mutate an object:

function updateColorMap(colormap) {
  colormap.right = 'blue';
}

However, we can avoid mutating the original object using Object.assign in ES6:

function updateColorMap(colormap) {
  return Object.assign({}, colormap, {right: 'blue'});
}

Or, we can use the spread operator for objects from ES2018:

function updateColorMap(colormap) {
  return {...colormap, right: 'blue'};
}

Immer

This is the basic syntax using Immer to avoid mutating data:

import produce from "immer"

const nextState = produce(baseState, draft => {
    draft[1].done = true
    draft.push({title: "Tweet about it"})
})

useState + Immer

Here is an example on CodeSandbox that uses the hook useState and Immer together.

import React, { useCallback, useState } from "react";
import produce from "immer";

const TodoList = () => {
  const [todos, setTodos] = useState([
    {
      id: "React",
      title: "Learn React",
      done: true
    },
    {
      id: "Immer",
      title: "Try Immer",
      done: false
    }
  ]);

  const handleToggle = useCallback((id) => {
    setTodos(
      produce((draft) => {
        const todo = draft.find((todo) => todo.id === id);
        todo.done = !todo.done;
      })
    );
  }, []);

  const handleAdd = useCallback(() => {
    setTodos(
      produce((draft) => {
        draft.push({
          id: "todo_" + Math.random(),
          title: "A new todo",
          done: false
        });
      })
    );
  }, []);

  return (<div>{*/ See CodeSandbox */}</div>)
}

useImmer

The state updaters have the same pattern, so they can be simplified to using the user-immer package.

import React, { useCallback } from "react";
import { useImmer } from "use-immer";

const TodoList = () => {
  const [todos, setTodos] = useImmer([
    {
      id: "React",
      title: "Learn React",
      done: true
    },
    {
      id: "Immer",
      title: "Try Immer",
      done: false
    }
  ]);

  const handleToggle = useCallback((id) => {
    setTodos((draft) => {
      const todo = draft.find((todo) => todo.id === id);
      todo.done = !todo.done;
    });
  }, []);

  const handleAdd = useCallback(() => {
    setTodos((draft) => {
      draft.push({
        id: "todo_" + Math.random(),
        title: "A new todo",
        done: false
      });
    });
  }, []);

  // SeeCodeSandbox 

useReducer and Redux + Immer

Similar to useState, Immer also combines with useReducer. And we have useImmerReducer method from the use-immer package.

For Redux + Immer, refer to this doc of Redux Toolkit:

Other readings on immutable data

A few points to note from this article:

  1. Avoid inline function definition in the render function

  2. Avoid using index as key for map

  3. Avoid using props in initial states

  4. Avoid spreading props on DOM elements

  5. Avoid async initialization in componentWillMount()

  6. Memoize React components

Last updated