React js 18 how to improve Rendering Performance ? with examples

Improving Rendering Performance

Although renders are the normal expected part of how React works, it's also true that that render work can be "wasted" effort at times. If a component's render output didn't change, and that part of the DOM didn't need to be updated, then the work of rendering that component was really kind of a waste of time.

React component render output should always be entirely based on current props and current component state. Therefore, if we know ahead of time that a component's props and state haven't changed, we should also know that the render output would be the same, that no changes are necessary for this component, and that we can safely skip the work of rendering it.

When trying to improve software performance in general, there are two basic approaches: 1) do the same work faster, and 2) do less work. Optimizing React rendering is primarily about doing less work by skipping rendering components when appropriate.

Component Render Optimization Techniques

React offers three primary APIs that allow us to potentially skip rendering a component:

  • React.Component.shouldComponentUpdate: an optional class component lifecycle method that will be called early in the render process. If it returns false, React will skip rendering the component. It may contain any logic you want to use to calculate that boolean result, but the most common approach is to check if the component's props and state have changed since last time, and return false if they're unchanged.
  • React.PureComponent: since that comparison of props and state is the most common way to implement shouldComponentUpdate, the PureComponent base class implements that behavior by default, and may be used instead of Component + shouldComponentUpdate.
  • React.memo(): a built-in "higher order component" type. It accepts your own component type as an argument, and returns a new wrapper component. The wrapper component's default behavior is to check to see if any of the props have changed, and if not, prevent a re-render. Both function components and class components can be wrapped using React.memo(). (A custom comparison callback may be passed in, but it really can only compare the old and new props anyway, so the main use case for a custom compare callback would be only comparing specific props fields instead of all of them.)

All of these approaches use a comparison technique called "shallow equality". This means checking every individual field in two different objects, and seeing if any of the contents of the objects are a different value. In other words, obj1.a === obj2.a && obj1.b === obj2.b && ......... This is typically a fast process, because === comparisons are very simple for the JS engine to do. So, these three approaches do the equivalent of const shouldRender = !shallowEqual(newProps, prevProps).

There's also a lesser-known technique as well: if a React component returns the exact same element reference in its render output as it did the last time, React will skip re-rendering that particular child. There's at least a couple ways to implement this technique:

  • If you include props.children in your output, that element is the same if this component does a state update
  • If you wrap some elements with useMemo(), those will stay the same until the dependencies change

Examples:

--

// The `props.children` content won't re-render if we update state
function SomeProvider({children}) {
  const [counter, setCounter] = useState(0);

  return (
    <div>
      <button onClick={() => setCounter(counter + 1)}>Count: {counter}</button>
      <OtherChildComponent />
      {children}
    </div>
  )
}

function OptimizedParent() {
  const [counter1, setCounter1] = useState(0);
  const [counter2, setCounter2] = useState(0);

  const memoizedElement = useMemo(() => {
    // This element stays the same reference if counter 2 is updated,
    // so it won't re-render unless counter 1 changes
    return <ExpensiveChildComponent />
  }, [counter1]) ;

  return (
    <div>
      <button onClick={() => setCounter1(counter1 + 1)}>Counter 1: {counter1}</button>      
      <button onClick={() => setCounter1(counter2 + 1)}>Counter 2: {counter2}</button>
      {memoizedElement}
    </div>
  )
}

--

For all of these techniques, skipping rendering a component means React will also skip rendering that entire subtree, because it's effectively putting a stop sign up to halt the default "render children recursively" behavior.

How New Props References Affect Render Optimizations

We've already seen that by default, React re-renders all nested components even if their props haven't changed. That also means that passing new references as props to a child component doesn't matter, because it will render whether or not you pass the same props. So, something like this is totally fine:

--

function ParentComponent() {
    const onClick = () => {
      console.log("Button clicked")
    }
    
    const data = {a: 1, b: 2}
    
    return <NormalChildComponent onClick={onClick} data={data} />
}

--

Every time ParentComponent renders, it will create a new onClick function reference and a new data object reference, then pass them as props to NormalChildComponent. (Note that it doesn't matter whether we're defining onClick using the function keyword or as an arrow function - it's a new function reference either way.)

That also means there's no point in trying to optimize renders for "host components", like a <div> or a <button>, by wrapping them up in a React.memo(). There's no child component underneath those basic components, so the rendering process would stop there anyway.

However, if the child component is trying to optimize renders by checking to see whether props have changed, then passing new references as props will cause the child to render. If the new prop references are actually new data, this is good. However, what if the parent component is just passing down a callback function?

--

const MemoizedChildComponent = React.memo(ChildComponent)

function ParentComponent() {
    const onClick = () => {
      console.log("Button clicked")
    }
    
    const data = {a: 1, b: 2}
    
    return <MemoizedChildComponent onClick={onClick} data={data} />
}

--

Now, every time ParentComponent renders, these new references are going to cause MemoizedChildComponent to see that its props values have changed to new references, and it will go ahead and re-render... even though the onClick function and the data object should be basically the same thing every time!

This means that:

  • MemoizedChildComponent will always re-render even though we wanted to skip rendering most of the time
  • The work that it's doing to compare its old and new props is wasted effort

Similarly, note that rendering <MemoizedChild><OtherComponent /></MemoizedChild> will also force the child to always render, because props.children is always a new reference.

Optimizing Props References

Class components don't have to worry about accidentally creating new callback function references as much, because they can have instance methods that are always the same reference. However, they may need to generate unique callbacks for separate child list items, or capture a value in an anonymous function and pass that to a child. Those will result in new references, and so will creating new objects as child props while rendering. React doesn't have anything built-in to help optimize those cases.

For function components, React does provide two hooks to help you reuse the same references: useMemo for any kind of general data like creating objects or doing complex calculations, and useCallback specifically for creating callback functions.

Memoize Everything?

As mentioned above, you don't have throw useMemo and useCallback at every single function or object you pass down as a prop - only if it's going to make a difference in behavior for the child. (That said, the dependency array comparisons for useEffect do add another use case where the child might want to receive consistent props references, which does make things more complicated.)

The other question that comes up all the time is "Why doesn't React wrap everything in React.memo() by default?".

Dan Abramov has repeatedly pointed out that memoization does still incur the cost of comparing props, and that there are many cases where the memoization check can never prevent re-renders because the component always receives new props. As an example, see this Twitter thread from Dan:

Why doesn’t React put memo() around every component by default? Isn’t it faster? Should we make a benchmark to check?

Ask yourself:

Why don’t you put Lodash memoize() around every function? Wouldn’t that make all functions faster? Do we need a benchmark for this? Why not?

Also, while I don't have a specific link on it, it's possible that trying to apply this to all components by default might result in bugs due to cases where people are mutating data rather than updating it immutably.

I've had some public discussion with Dan about this on Twitter. I personally think it's likely that using React.memo() on a widespread basis would likely be a net gain in overall app rendering perf. As I said in an extended Twitter thread last year:

The React community as a whole seems to be over obsessed with "perf", yet much of the discussion revolves around outdated "tribal wisdom" passed down via Medium posts and Twitter comments rather than based on concrete usage.

There's definitely collective misunderstanding about the idea of a "render" and the perf impact. Yes, React is totally based around rendering - gotta render to do anything at all. No, most renders aren't overly expensive.

"Wasted" rerenders certainly aren't the end of the world. Neither is rerendering the whole app from the root. That said, it's also true that a "wasted" rerender with no DOM update is CPU cycles that didn't need to be burned. Is that a problem for most apps? Probably not. Is it something that can be improved? Probably.

Are there apps where default "rerender it all" approaches aren't sufficient? Of course, that's why sCU, PureComponent, and memo() exist.

Should users wrap everything in memo() by default? Probably not, if only because you should think about your app's perf needs. Will it actually hurt if you do? No, and realistically I expect it does have a net benefit (despite Dan's points about wasted comparisons)

Are benchmarks flawed, and results highly variable based on scenarios and apps? Of course. That said, it would be REALLY REALLY HELPFUL if folks could start pointing at hard numbers for these discussions instead of playing the telephone game of "I saw a comment once..."

I'd love to see a bunch of benchmark suites from the React team and the larger community to measure a bunch of scenarios so we could stop arguing about most of this stuff once and for all. Function creation, render cost, optimization... CONCRETE EVIDENCE, PLEASE!

But, no one's put together any good benchmarks that would demonstrate whether or not this is true:

Dan's standard answer is that app structure and update patterns vary drastically, so it's hard to make a representative benchmark.

I still think some actual numbers would be useful to aid the discussion

There's also an extended issue discussion on "When should you NOT use React.memo? in the React issues.

(And yes, this blog post is basically a long-delayed and much-expanded version of that tweet thread, although I'd actually forgotten I'd tweeted all that until I ran across it just now while researching the post.)

Immutability and Rerendering

State updates in React should always be done immutably. There are two main reasons why:

  • depending on what you mutate and where, it can result in components not rendering when you expected they would render
  • it causes confusion about when and why data actually got updated

Let's look at a couple specific examples.

As we've seen, React.memo / PureComponent / shouldComponentUpdate all rely on shallow equality checks of the current props vs the previous props. So, the expectation is that we can know if a prop is a new value, by doing props.someValue !== prevProps.someValue.

If you mutate, then someValue is the same reference, and those components will assume nothing has changed.

Note that this is specifically when we're trying to optimize performance by avoiding unnecessary re-renders. A render is "unnecessary" or "wasted" if the props haven't changed. If you mutate, the component may wrongly think nothing has changed, and then you wonder why the component didn't re-render.

The other issue is the useState and useReducer hooks. Every time I call setCounter() or dispatch(), React will queue up a re-render. However, React requires that any hook state updates must pass in / return a new reference as the new state value, whether it be a new object/array reference, or a new primitive (string/number/etc).

React applies all state updates during the render phase. When React tries to apply a state update from a hook, it checks to see if the new value is the same reference. React will always finish rendering the component that queued the update. However, if the value is the same reference as before, and there are no other reasons to continue rendering (such as the parent having rendered), React will then throw away the render results for the component and bail out of the render pass completely.