How to optimize React using React DevTools

React as a front end view library is plenty fast for most situations. However, there are times when certain user flows become slow and we need to optimize the problematic components. We will be able to investigate and solve this issue using React DevTools.

What is React DevTools?

The React DevTools is the official browser extension available for Chrome, Firefox and Edge. It is a set of tools that enables us to debug and analyze React components, such as their hierarchy and performance.

Installation and usage

Once you have installed the extension, you can open up the browser devtool on any webpage that is currently using React. You will see two panels introduced by React DevTools.

Components panel

Image of React DevTools' Components panel

The first tool is the Components panel. It allows you to visualize the React component hierarchy, as well as the various props and state for that component.

Profiler panel

Image of React DevTools' Profiler panel

The Profiler panel is the main tool that we will be using to optimize the slow React components. After recording a certain flow, it will generate a flame graph as seen above.

The flame graph is a visualization of the time spent rendering for each component, which includes time rendering its children. That is why the Root component takes up the entire first row in the graph, with each subsequent child being shorter.

One of the more confusing aspects of reading this graph is the understanding that only the colored components are the ones that have rendered during the current frame. The length of the grayed out components is the amount of time they took to render before profiling.

Image of React DevTools' Profiler bar chart

The top right-hand corner comes with a bar chart. Each bar represents a render, and the height and color represents the duration it took to render. If you hover over the bar, it tells you how long the phase took and when it started.

The right commit information sidebar also lists some useful data:

  • Priority: Priority of the rendering component, which might be different under concurrent mode
  • Committed at: The React commit phase time since pressing the record button
  • Render duration: The amount of time needed to finish rendering after the commit phase
  • Interactions: User interactions such as clicks

How to find slow components

Image of React DevTools' Profiler Hide commits panel

The fastest way to find components that need immediate addressing is to first hide commits below a certain threshold. Ideally, we would like to achieve a smooth 60fps for our UI, which means that any commit over 16ms would result in a dropped frame. However, we as developers generally have the best equipment available, so reducing this threshold to 6-8ms would be a more realistic representation of our users' experience.

Image of React DevTools' Profiler Ranked panel

After recording a session, we can navigate through all the rendering phases that took more than our desired threshold in the top right corner of the profiler. The slowest phases will be taller and more yellow. Aside from viewing the flame graph, we can also use the "Ranked" subpanel to find out which components in the tree took the longest time to render.

For most cases, this would be how you find the problematic components but there are times when you see a profile where every phase is fast but the number of phases goes beyond hundreds.

Optimizing React components

There are three common reasons why a component is slow. We will go through each case and suggest solutions for them.

Fixing components that re-render needlessly

When a component has a lot of child components, re-rendering the entire subtree becomes expensive, so we want to avoid doing so if it is not necessary.

Unnecessary re-renders are identified by noticing that props, state, and hooks results are the same as the previous render. The Why Did You Render library can be used to automate this step.

In the example below, the ExpensiveComponent re-renders whenever the increment button is clicked, even though it doesn't need to.

function App() {
  const [counter, setCounter] = useState(0);

  return (
    <>
      <button onClick={() => setCounter((count) => count + 1)}>
        increment
      </button>
      <ExpensiveComponent style={{ display: 'flex' }} />
    </>
  );
}

function ExpensiveComponent(props) {
  return (
    <ul style={props.style}>
      {Array(500)
        .fill()
        .map((val) => (
          <li>val</li>
        ))}
    </ul>
  );
}

Fixing this issue is relatively straightforward. First, you want to ensure that props, state and hook results are all shallowly equal throughout re-renders. This could be as easy as using the useMemo hook, like so:

function App() {
  const [counter, setCounter] = useState(0);
  const style = useMemo(() => ({ display: 'flex' }));

  return (
    <>
      <button onClick={() => setCounter((count) => count + 1)}>
        increment
      </button>
      <ExpensiveComponent style={style} />
    </>
  );
}

Secondly, wrap the topmost level component with React.memo, so that when the component receives props that are shallowly equal with the previous render, it will skip rendering.

const ExpensiveComponent = React.memo((props) => {
  return (
    <ul style={props.style}>
      {Array(500)
        .fill()
        .map((val) => (
          <li>val</li>
        ))}
    </ul>
  );
});

Fixing expensive components

This is the situation where your component either has a lot of child components, or the component is doing some very heavy calculations. This is hard to solve because your solutions are limited.

If your component is taking a lot of time because it renders many child components, you could look into using virtualization (also known as windowing) libraries like react-window and react-virtualized. They work around the issue by rendering only child components that appear in the viewport, rather than all the child components at once.

If the component is doing heavy calculations, depending on how intensive the calculation is, you could look into running the calculations on the server-side or in a Web Worker.

Otherwise, you are stuck with what you have, but React's ongoing work on Concurrent Mode might alleviate this issue in the future.

Fixing components that re-render too many times

Image of React DevTools' Profiler rendering too many times

Their telltale sign is when the profile has hundreds of frames. Components that re-render too often usually do not affect the user's experience, but they cause unnecessary power usage and make profiling harder by filling up the bar chart in the top right.

They are also usually caused by animation or timers, like the example below:

function Spinner() {
  const [degrees, setDegrees] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setDegrees((deg) => (deg + 1) % 360);
    }, 16);
    return () => clearInterval(id);
  }, []);

  return <SpinSVG style={{ transform: `rotate(${degrees}deg)` }} />;
}

In this scenario, it may be worth moving the animation logic outside of React. This also results in more efficient code as it bypasses React's diffing logic.

function Spinner() {
  const divRef = useRef();

  useEffect(() => {
    let degrees = 0;
    const id = setInterval(() => {
      degrees = (degrees + 1) % 360;
      divRef.current.style.transform = `rotate(${degrees}deg)`;
    }, 16);
    return () => clearInterval(id);
  }, []);

  return <SpinSVG ref={divRef} />;
}

Conclusion

By keeping the UI fast, not only do you make your users' experience much more enjoyable, but you also make debugging and improving the application easier in the future. Finding and optimizing slow components using React DevTools can be quite easy once you have mastered the tool. So go out there and start improving your app!