Performance in React

Here are some notes about performance in React.

They come from the book The Road to React by Robin Wieruch.

Strict Mode

Enable Strict Mode:

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)

Strict Mode:

  • warns in dev tools when using a deprecated React API (e.g. a legacy React hook)
  • ensures that state and side-effects are implemented well:
    • it renders components twice (on dev but not production) in order to detect any problems
    • it runs useEffect twice for the initial render
    • the React core team decided that this behavior is needed for surfacing bugs related to misused side-effects
    • beware of impure components

Don't call useEffect for the first render (useRef)

There is no way out of the box to run useEffect only on every re-render but not on the first render (mount).

To avoid calling useEffect for the first render, use useRef which keeps its ref.current property intact over re-renders:

const useStorageState = (key, initialState) => {
  const isMounted = React.useRef(false);

  const [value, setValue] = React.useState(
    localStorage.getItem(key) || initialState
  );

  React.useEffect(() => {
    if (!isMounted.current) {
      isMounted.current = true;
    } else {
      console.log('A', value);
      localStorage.setItem(key, value);
    }
  }, [value, key]);

  return [value, setValue];
};

This technique is practical to avoid unnecessary function invocations.

Trick for logging in a component without a function body

console.log() always evaluates to false so the right-hand side of the operator gets always executed:

const List = ({ list, onRemoveItem }) => (
  console.log('B:List') || (<ul>
    …
  </ul>)
)

Don't re-render if not needed (memo and useCallback)

If a parent component re-renders, its descendent components re-render as well.

React does this by default, because preventing a re-render of child components could lead to bugs.

Because the re-rendering mechanism of React is often fast enough by default, the automatic re-rendering of descendent components is encouraged by React.

Sometimes we want to prevent re-rendering (e.g. huge data sets displayed in a table shouldn't re-render if they are not affected by an update).

This can be done with React's memo API which checks whether the props of a component have changed:

const List = React.memo(
  ({ list, onRemoveItem }) => (
    console.log('B:List') || (<ul>
      {list.map(item => (
        <Item
          key={item.objectID}
          item={item}
          onRemoveItem={onRemoveItem}
        />
      ))}
    </ul>)
  )
);

If the props haven't changed, it does not re-render even though its parent component re-rendered.

But here, we must handle onRemoveItem too with useCallback because when the App component re-renders, it always creates a new version of this callback handler (handleRemoveStory) as a new function:

const handleRemoveStory = React.useCallback((story) => {
  dispatchStories({
    …
  });
}, []);;

To summarize, if re-rendering decreases the performance of a React application:

  • React's memo API helps prevent re-rendering
  • sometimes memo alone doesn't help because callback handlers are re-defined each time in the parent component and passed as changed props to the component, which causes another re-render
  • in that case, useCallback is used for making the callback handler only change when its dependencies change

Don't rerun expensive computations (useMemo)

In case of cost expensive computations which could lead to a re-rendering delay (here we have non-heavy computation but imagine one that would take more than 500ms):

const getSumComments = (stories) => {
  return stories.data.reduce(
    (result, value) => result + value.num_comments,
    0
  );
};

const App = () => {
  …
  const sumComments = getSumComments(stories);

  return (
    <StyledContainer>
      <StyledHeadlinePrimary>My Hacker Stories with {sumComments} comments</StyledHeadlinePrimary>
      …
    </StyledContainer>
  );
}

Note:

  • a function can be outside the component when it does not have any dependency from within the component
  • this prevents creating the function on every render, so the useCallback hook becomes unnecessary

The function computes the value of summed comments on every render.

To tell React to only run a function if one of its dependencies has changed, use useMemo:

const App = () => {
  …
  const sumComments = React.useMemo(
    () => getSumComments(stories),
    [stories]
  );

  return (
    <StyledContainer>
      <StyledHeadlinePrimary>My Hacker Stories with {sumComments} comments</StyledHeadlinePrimary>
      …
    </StyledContainer>
  );
}

Now, it only runs if the dependency array (here stories) has changed.

useMemo, useCallback, and memo

These shouldn't necessarily be used by default.

Apply these performance optimizations only if you run into performance bottlenecks.

Most of the time this shouldn't happen, because React's rendering mechanism is pretty efficient by default.

Sometimes the check for utilities like memo can be more expensive than the re-rendering itself.

Avant Fundamentals of React Après TypeScript in React

Tag Kemar Joint