Few Tips to Optimizing Performance of React Project

oahehc

Andrew

Posted on February 2, 2020

Few Tips to Optimizing Performance of React Project
In this article, I will introduce a few different ways to optimize the performance for React apps. And use an example to demonstrate how to choose a proper solution when doing the optimization.

Before we start, let's look at an example.
Here we have 24 checkboxes to allow the users to select the time they want.

The problem of this example is that every time when the user clicks the checkbox, all the checkboxes will also re-render.

So, how can we solve this problem?

React DevTools Profiler

Before we start optimizing our application, we need to know how to identify the performance issues of our application?
react-dom 16.5+ and react-native 0.57+ provide enhanced profiling with the React DevTools Profiler.

Using React DevTools Profiler is simple, click the record button at the top-left corner, interact with our application and stop the record by clicking the same button again. Then we ended up having the result to identify the issues.
react-profiler

When we check the flamegraph through React DevTools Profiler, we can see the un-necessary re-render.
react-profiler

Now we know the problem, let's try a few different solutions for that.

PureComponent

First, we can try the simplest solution - PureComponent, we just need to change our class extend from component to PureComponent, then React will do the rest for us.

// before
export default class CheckBox extends React.Component {
  ...
}

// after
export default class CheckBox extends React.PureComponent {
  ...
}
Enter fullscreen mode Exit fullscreen mode

But after we change to PureComponent, we can see it didn't prevent the unnecessary re-render. The reason is that we create a new handleToggle function every time. So even we apply PureComponent it still re-render all the CheckBox components when App component re-render.

ShouldComponentUpdate

Because PureComponent didn't work. So now we have to do the check on our own. We can use ShouldComponentUpdate to block un-necessary render.

shouldComponentUpdate(nextProps) {
  const {value, isChecked} = nextProps;
  return this.props.value !== value || this.props.isChecked !== isChecked
}
Enter fullscreen mode Exit fullscreen mode

Now, when we check React DevTools Profiler again, we will see only the click checkbox will re-render.
react-profiler

React.memo

If we want to use function components rather then class, we have another option - React.memo.
React.memo will do the same check as PureComponent. But it allows us to pass the second parameter to do the custom check similar with ShouldComponentUpdate. But we need to notice that the return value should be opposite with ShouldComponentUpdate.

export default React.memo(CheckBox, (prevProps, nextProps) => {
  return prevProps.value === nextProps.value && prevProps.isChecked === nextProps.isChecked
});
Enter fullscreen mode Exit fullscreen mode

useMemo

Another solution for function components is to use hooks - useMemo.

export default function CheckBox ({value, isChecked, handleToggle}){
  return React.useMemo(() => {
    return (
      <div>
        <label>
          <input type="checkbox" value={value} checked={isChecked} onChange={handleToggle} />
          {value}
        </label>
      </div>
    )
  }, [value, isChecked]);
}
Enter fullscreen mode Exit fullscreen mode

Although this can help us prevent un-necessary re-render. We will see an error from eslint-plugin-react-hooks.
eslint-react-hooks

The eslint will remind us to add handleToggle into our dependency array. But we can't because that's something we have to ignore to prevent un-necessary re-render. We can easily use eslint-disable to prevent this error. But actually, this error message does point out an important problem.

Although most of the above solutions (except PureComponent) can help us optimize the performance. But those custom logic also make the code harder to maintain and might bring some potential bugs.
Let's say when another team member adds a new prop - isDarkMode for the Checkbox component, if he or she forgets to adjust the custom logic in ShouldComponentUpdate or React.memo, then the dark mode won't work because Checkbox will not re-render when isDarkMode prop change.

So, how can we solve this problem?

useCallback

A better way to solve this performance problem is to prevent creating a new handleToggle function every time.
We can change our App component into a class component. Or use another hook - useCallback to do the job.

const handleToggle = useCallback(targetTime => {
  setTimeCheckboxes(timeCheckBoxes => {
    return timeCheckBoxes.map(({ time, isChecked }) => ({
      time,
      isChecked: targetTime === time ? !isChecked : isChecked
    }));
  });
}, []);
Enter fullscreen mode Exit fullscreen mode

Due to we won't create a new toggle function every time now. We just need to apply PureComponent on Checkbox. Then we can prevent the un-necessary re-render without adding any custom logic into our codebase.

Measurement

Furthermore, rather than just understanding how to optimize our application, we also need to know how to measure the performance of our application.

React Profiler

React provide a component to help us achieve that - Profiler.
Just simply wrap our App component with Profiler, then we can get the information we need.

<Profiler id="app" onRender={onRenderCallback}>
  <div className="App">
    ...
  </div>
</Profiler>
Enter fullscreen mode Exit fullscreen mode

onRender prop will pass those information into our callback function. So we can print the information we need.

function onRenderCallback(
  id, // the "id" prop of the Profiler tree that has just committed
  phase, // either "mount" (if the tree just mounted) or "update" (if it re-rendered)
  actualDuration, // time spent rendering the committed update
  baseDuration, // estimated time to render the entire subtree without memoization
  startTime, // when React began rendering this update
  commitTime, // when React committed this update
  interactions // the Set of interactions belonging to this update
) {
  // Aggregate or log render timings...
}
Enter fullscreen mode Exit fullscreen mode

Now that we can know the difference before and after the optimization.

  • Before
    profiler

  • After (apply useCallback and PureComponent)
    profiler

Chrome DevTools: Performance

Another option is to use Chrome dev-tools. We can select Performance tab and start record just like what we did on React DevTools Profiler.
(here I slow down the CPU to make it easier to identify the performance problem, we can also simulate a slower network if we need)
chrome-dev-tools

Then we can see the result like this.

  • Before: 152.72ms, 132.22ms, 204.83ms chrome-dev-tools
  • After (apply useCallback and PureComponent): 15.64ms, 18.10ms, 12.32ms chrome-dev-tools

Conclusion

React provide many APIs and tools to help us optimize our application. While we try to optimize the performance, we need to choose the solution wisely, this will make sure our code is still easy to maintain after we improve the performance of our application.

--

Reference

💖 💪 🙅 🚩
oahehc
Andrew

Posted on February 2, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related