wall painting

Optimizing Renders in React

Chrome Extensions Here to Help!

If you have the React Dev Tools extension installed for Chrome and go to Components -> Settings -> General, a menu will come up. Check the box that says Highlight updates when components render:

react dev tools chrome

Go to your app you'll be able to see what components are firing off renders with a green border that flashes momentarily:

render gif

React does a really good job behind the scenes preparing for screen repaints using a VDOM (Virtual DOM). You don't really have to do anything because React optimizes native DOM renders to minimize rendering as much as possible.

If you need an intro or want to learn more about JSX then you can go here.

Say we had the below React JSX for a simple form component:

const InputComponent = ({value}) => {
  return <input type="text" value={value} />;
};

const ButtonComponent = ({onClick, btnText}) => {
  return <button onClick={onClick}>{btnText}</button>;
};

const App = () => {
  const [text, setText] = useState('');

  const onClick = () => {
    setText('');
  };

  return (
    <div>
      <InputComponent value={text} />
      <ButtonComponent btnText="Clear text" onClick={onClick} />
    </div>
  );
};

When App executes React is going to receive an object describing to the native DOM in your browser how it should put the elements on the page. This is the abbreviated version since the actual object is a bit more complicated:

const appElement = {
  type: 'div', // describes the parent div element
  props: {
    children: [
      {
        type: 'input', // describes the input element
        props: {
          type: 'text',
          value: '',
        },
      },
      {
        type: 'button', // describes the button element
        props: {
          btnText: 'Clear text',
          onClick: onClick,
        },
      },
    ],
  },
};

Say we click the button 10 times in a row. You might assume that a render will be triggered. But the onClick is setting the value of the input to the same thing over and over again, which is an empty string. Since the value never changes, the object describing the App component repeatedly returns the same snapshot from the previous render. This tells React that there is no need to render the component again.

Felix Gerschau writes a good article about this.

We Don't Really Need to Do Much to Get Performance Out of React

Sometimes you'll run into instances where needless renders will occur. And for these cases there are a few things you can try if you're having performance issues.

Use React.memo

React.memo can help reduce renders. Suppose we have the following:

import React, {useState} from 'react';

const Counter = () => {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p> Count: {count}</p>
      <button onClick={incrementCount}>increment</button>
    </div>
    );
};

const App = () => {
  const [text, setText] = useState('');
  
  const handleSetText = (e) => {
    setText(e.target.value);
  };
  
  const incrementCount = () => {
    setCount((prevCount) => prevCount + 1);
  };

  return (
    <div className="container">
      <input type="text" value={text} onChange={handleSetText} />
      <Counter incrementCount />
    </div>
  );
};

export default App;

Here we have a text input that changes the text state and counter component that changes the count state.

counter input component

Now since the counter component is an adjacent sibling to the input component, it will re-render every time the input is re-rendered even though nothing in the counter component has actually changed. We can see this in action by using refs.

import React, {useState, useRef} from 'react';

const Counter = () => {
  const [count, setCount] = useState(0);
  const renders = useRef(0);

  const incrementCount = () => {
    setCount((prevCount) => prevCount + 1);
  };

  return (
    <div>
      <p> Renders: {renders.current++}</p>
      <p> Count: {count}</p>
      <button onClick={incrementCount}>increment</button>
    </div>
  );
};

As you can see, we've imported the useRef hook and set its initial value to 0. The renders reference returns a property called current that reflects this value and now every time that component is ran, renders.current will increment.

counter input renders

See how even though the count hasn't changed at all the count component is still re-rendering. This is where React.memo comes to the rescue. We're going to wrap the counter with React.memo. What this will do is keep track of the props being passed to the counter component and if there are no changes from the previous render then the component will not be re-rendered:

import React, {useState, useRef} from 'react';

const Counter = React.memo(() => {
  const [count, setCount] = useState(0);
  const renders = useRef(0);

  const incrementCount = () => {
    setCount((prevCount) => prevCount + 1);
  };

  return (
    <div>
      <p> Renders: {renders.current++}</p>
      <p> Count: {count}</p>
      <button onClick={incrementCount}>increment</button>
    </div>
  );
});

react memo counter

And there it is! Notice how renders does not increment when we type something in the input. This means that the counter is not re-rendering on every keystroke.

Key Takeaways

  • React Chrome Dev Tools can help you spot when and where renders are occurring.
  • React is super efficient at handling screen repaints so you rarely have to do anything to optimize renders.
  • When you do notice performance issues, you can try using React.memo to remove bottlenecks caused be massive re-rendering.