The source code for this blog is available on GitHub.

Blog.

React Performance Recipes

Problem: Unnecessary Callback Re-Initiation

When you're using a callback function within useEffect, it's hard to predict how callback function will be modified in the future. Only passing in certain variables from the callback to the deps list won't do. Because once the variables passed in gets removed, your dependencies won't work any more. But you still have to provide some values to the dependency list to sync useEffect with the callback change.

One way we can solve this is to include the entire callback function into the dependency list.

But doing this also introduces another problem -- because the callback function is defined in the component's function body, it will be re-initialized from scratch every time the component gets rendered, and since functions are compared through references, even if the code of a function isn't modified between renders, every time it gets re-initialized, it will be different from it "previous" self in the last render of the component, which will trigger a infinite re-rendering loop that you definitely wouldn't like. And that difference is what some people call "referential inequality", which will also happen when you're working with objects and arrays, because these three types are structural types that are compared through references.

Solution: React.useCallback

const memoizedCallback = React.useCallback(callback, dependencyList): Memoizes a callback function, only re-initialized it when the dependencies change and avoids referential inequality.

And now every time the component gets re-rendered, the updateLocalStorage callback will only re-initialized when the value changes, otherwise React will use the same copy of it.

useCallback can be powerful if properly used, but it won't necessarily make a callback function better for performance. As you can see, it and the memoized one won't be garbage collected after the render, which is something you should be aware of. So remember to only useCallback when you have to.

When you're using a callback function within useEffect, it's hard to predict how callback function will be modified in the future. Only passing in certain variables from the callback to the deps list won't do. Because once the variables passed in gets removed, your dependencies won't work any more. But you still have to provide some values to the dependency list to sync useEffect with the callback change.

App.js
window.localStorage.setItem('count', count)
window.localStorage.setItem('count', count)

One way we can solve this is to include the entire callback function into the dependency list.

App.js
}, [updateLocalStorage]) // <-- function as a dependency
}, [updateLocalStorage]) // <-- function as a dependency

But doing this also introduces another problem -- because the callback function is defined in the component's function body, it will be re-initialized from scratch every time the component gets rendered, and since functions are compared through references, even if the code of a function isn't modified between renders, every time it gets re-initialized, it will be different from it "previous" self in the last render of the component, which will trigger a infinite re-rendering loop that you definitely wouldn't like. And that difference is what some people call "referential inequality", which will also happen when you're working with objects and arrays, because these three types are structural types that are compared through references.

App.js
}, [updateLocalStorage]) // changes in every render
}, [updateLocalStorage]) // changes in every render

Solution: React.useCallback

const memoizedCallback = React.useCallback(callback, dependencyList): Memoizes a callback function, only re-initialized it when the dependencies change and avoids referential inequality.

And now every time the component gets re-rendered, the updateLocalStorage callback will only re-initialized when the count value changes, otherwise React will use the same copy of it.

App.js
() => window.localStorage.setItem('count', count),
() => window.localStorage.setItem('count', count),

useCallback can be powerful if properly used, but it won't necessarily make a callback function better for performance. As you can see, it causes extra memory allocation and the memoized one won't be garbage collected after the render, which is something you should be aware of. So remember to only useCallback when you have to.

App.js
const memoizedCallback = React.useCallback(callback, [someValue])
const memoizedCallback = React.useCallback(callback, [someValue])

Problem: Unnecessary Expensive Calculation

React comes with the concept of "rendering". When a component is being rendered by React, that means React is calling the component function (or the render method in the case of class components). This comes with an unfortunate limitation that calculations performed within the function (or render) will be performed every single render, regardless of whether the inputs for the calculations change. For example:

Every time the parent component of MyComponent is re-rendered or some state is added later and setting the state triggers re-renders, so for expensiveCalculation, of this component, which leads to performance bottlenecks.

Solution: React.useMemo

const memoizedResult = React.useMemo(calculationCallback, dependencyList): Memoizes the result of the calculation and only calls the calculation when the dependencies changes.

With , here is the optimized version of MyComponent. And now result will only be calculated again when x or y changes.

React comes with the concept of "rendering". When a component is being rendered by React, that means React is calling the component function (or the render method in the case of class components). This comes with an unfortunate limitation that calculations performed within the function (or render) will be performed every single render, regardless of whether the inputs for the calculations change. For example:

App.js
return <div>Hello World</div>
return <div>Hello World</div>

Every time the parent component of MyComponent is re-rendered or some state is added later and setting the state triggers re-renders, so for expensiveCalculation, it will be recalculated in every render of this component, which leads to performance bottlenecks.

App.js
const result = expensiveCalculation(x, y)
const result = expensiveCalculation(x, y)

Solution: React.useMemo

const memoizedResult = React.useMemo(calculationCallback, dependencyList): Memoizes the result of the calculation and only calls the calculation when the dependencies changes.

With useMemo, here is the optimized version of MyComponent. And now result will only be calculated again when x or y changes.

App.js
const result = React.useMemo(() => expensiveCalculation(x, y), [x, y])
const result = React.useMemo(() => expensiveCalculation(x, y), [x, y])

So when should I useMemo and useCallback?

Use useCallback when you want to avoid unnecessary re-renders caused by referential inequality of structural data types, for example:

When using as in useEffect.

When using to avoid unnecessary child re-renders when you pass complex as event handlers into child components. Because callback functions will be re-initialized every re-render.

Use useMemo when you want to avoid unnecessary computationally expensive .

When using values as dependencies in useEffect.

App.js
const baz = React.useMemo(() => [1, 2, 3], [])
const baz = React.useMemo(() => [1, 2, 3], [])

When using React.memo to avoid unnecessary child re-renders when you pass complex callbacks as event handlers into child components. Because callback functions will be re-initialized every re-render.

App.js
const CountButton = React.memo(function CountButton({onClick, count}) {
const CountButton = React.memo(function CountButton({onClick, count}) {

Use useMemo when you want to avoid unnecessary computationally expensive calculations.

App.js
() => calculatePrimes(iterations, multiplier),
() => calculatePrimes(iterations, multiplier),

Problem: Unnecessary Child Re-Renders

Besides function re-initiation and value recalculation, there is another behavior that can lead to performance bottlenecks: unnecessary re-renders. As I said before, rendering means that React is calling your component function or its render method. React renders a component to recalculate the data to reflect the changes within the component. In what circumstances will a re-render be unnecessary?

There are four reasons for React to re-render a component:

  1. The state of the component has changed, which must be triggered with the state setting function.
  2. The props the component receives have changed.
  3. The context values the component uses have changed.
  4. The component's parent has been re-rendered because of the reasons above.

Now the first three can't and also shouldn't be avoided because those are basically data changes, which must be recalculated and displayed on the screen. But the last one is, most of the time, unnecessary. When a parent component changes, if there is no change to be applied on the child, there is no need to call the component function to reflect any data changes.

Here is an example:

When you click the , it changes the of its parent Example, which triggers a re-render for Example, which in turn leads to the unnecessary re-render of the even though none of its props has changed.

So how do we solve this problem?

Solution: React.memo

React.memo(component): memoize a component and only re-render it when the props it receives changes.

To prevent the unnecessary re-render of NameInput, we can wrap it with React.memo as a bail-out. Like :

By doing this we let React know that it doesn't need a re-render until at least one of its props changes.

React.memo: Might not work like a charm

You might want to ask what if we do this to the CountButton. Here is the answer: If we were to only wrap it into React.memo like , it wouldn't work, because increment always gets re-initialized every time Example gets re-rendered, which makes the onClick prop of CountButton changes all the time due to referential inequality. But React.memo only prevent re-renders when all the props stay the same.

Unless we also wrap the increment function within the Example with React.useCallback. Like :

Just so you know that it's better to use React.memo more mindfully rather than shove everything into it.

Here is an example:

App.js
<div>{`${name}'s favorite number is ${count}`}</div>
<div>{`${name}'s favorite number is ${count}`}</div>

When you click the CountButton, it changes the state of its parent Example, which triggers a re-render for Example, which in turn leads to the unnecessary re-render of the NameInput even though none of its props has changed.

So how do we solve this problem?

Solution: React.memo

React.memo(component): memoize a component and only re-render it when the props it receives changes.

App.js
<div>{`${name}'s favorite number is ${count}`}</div>
<div>{`${name}'s favorite number is ${count}`}</div>

To prevent the unnecessary re-render of NameInput, we can wrap it with React.memo as a bail-out. Like this:

App.js
<div>{`${name}'s favorite number is ${count}`}</div>
<div>{`${name}'s favorite number is ${count}`}</div>

By doing this we let React know that it doesn't need a re-render until at least one of its props changes.

React.memo: Might not work like a charm

You might want to ask what if we do this to the CountButton. Here is the answer: If we were to only wrap it into React.memo like this, it wouldn't work, because increment always gets re-initialized every time Example gets re-rendered, which makes the onClick prop of CountButton changes all the time due to referential inequality. But React.memo only prevent re-renders when all the props stay the same.

App.js
<div>{`${name}'s favorite number is ${count}`}</div>
<div>{`${name}'s favorite number is ${count}`}</div>

Unless we also wrap the increment function within the Example with React.useCallback. Like this:

Just so you know that it's better to use React.memo more mindfully rather than shove everything into it.

App.js
const increment = React.useCallback(() => setCount((c) => c + 1), [])
const increment = React.useCallback(() => setCount((c) => c + 1), [])

React.memo: Use a custom comparator function

Most of the time, a simple React.memo works just fine, there are times when its default behavior isn't what you desire.

Imagine you are rendering a menu containing a list of items, and the user can highlight a item at a time. Your Menu and ListItem components might look like .

Both of these components are wrapped into React.memo . So now you might expect this way the performance has been optimized to its highest level. But here is a problem still unsolved: when the user highlights a different item, besides the the previously and newly highlighted items, all the other untouched items will still be re-rendered. And that's because every time the user highlights a different item, they changes the highlightedIndex prop, which triggers the re-render of each item, no matter if it was highlighted previously or is highlighted now or never has been touched the whole time.

To solve this problem, you'll need to change how React should compare props over time. And this is where the custom comparator comes into play. You can pass a custom comparator as the second argument into React.memo so that it uses your, hopefully better, rules, instead of its default rules, for prop comparison. For example, for the Menu and ListItem, we can do :

This way the ListItem won't be re-rendered unless it is involved in highlight changes.

React.memo: Parent keeps the calculation, children keep the primitive props

You might notice the logic in the custom comparator above is quite mind-boggling, especially when you are working on a big project with a team of many people. So for that scenario, one takeaway you might find valuable is that, if you're rendering a ton of instances of a particular component, try to do calculations a little higher -- maybe in the parent or even higher -- so you only need to pass primitive values to the component and let those value changes trigger DOM updates. That way you won't have to worry about breaking memoization or create a custom comparator, which can save you quite some code and brain cells. For example, we can simplify the code above like :

Notice how we move the details of the highlight and selection calculation up to the parent component Menu, only pass two boolean props isHighlighted and isSelected to the child component ListItem and don't even have to write a custom comparator function.

React.memo: Use a custom comparator function

Most of the time, a simple React.memo works just fine, there are times when its default behavior isn't what you desire.

Imagine you are rendering a menu containing a list of items, and the user can highlight a item at a time. Your Menu and ListItem components might look like this.

Both of these components are wrapped into React.memo already. So now you might expect this way the performance has been optimized to its highest level. But here is a problem still unsolved: when the user highlights a different item, besides the the previously and newly highlighted items, all the other untouched items will still be re-rendered. And that's because every time the user highlights a different item, they changes the highlightedIndex prop, which triggers the re-render of each item, no matter if it was highlighted previously or is highlighted now or never has been touched the whole time.

To solve this problem, you'll need to change how React should compare props over time. And this is where the custom comparator comes into play. You can pass a custom comparator as the second argument into React.memo so that it uses your, hopefully better, rules, instead of its default rules, for prop comparison. For example, for the Menu and ListItem, we can do this:

This way the ListItem won't be re-rendered unless it is involved in highlight changes.

React.memo: Parent keeps the calculation, children keep the primitive props

You might notice the logic in the custom comparator above is quite mind-boggling, especially when you are working on a big project with a team of many people. So for that scenario, one takeaway you might find valuable is that, if you're rendering a ton of instances of a particular component, try to do calculations a little higher -- maybe in the parent or even higher -- so you only need to pass primitive values to the component and let those value changes trigger DOM updates. That way you won't have to worry about breaking memoization or create a custom comparator, which can save you quite some code and brain cells. For example, we can simplify the code above like this:

App.js
backgroundColor: isHighlighted ? 'lightgray' : 'inherit',
backgroundColor: isHighlighted ? 'lightgray' : 'inherit',

Notice how we move the details of the highlight and selection calculation up to the parent component Menu, only pass two boolean props isHighlighted and isSelected to the child component ListItem and don't even have to write a custom comparator function.

Problem: Rendering huge lists with large quantities of data

As we learned before, React is pretty optimized itself and provides a suite of performance optimization tools for you to use. But if you were to make huge updates to the DOM, there is little React can do because there is just too much to do. This problem is always revealed by UIs like data visualization, grids, tables, and lists with lots of data. There’s only so much you can do before we have to conclude that we’re simply running too much code (or running the same small amount of code too many times).

Solution: Windowing

But it's still possible for us to work around this: when the user scroll through a huge table, chances are that they are only gonna see a portion of it, and that portion they are viewing won't be bigger than their window size. So what we can do is just to fetch a tiny part of the whole data set and render only for that portion exposed to the user, and as the user scroll through the list, we just "lazy" fetch the additional data, render the newly needed UI, and destroy the unwanted part just in time. Because the "lazy" rendered part of the UI is no bigger than the user's window size, this technique is known as "windowing", aka "virtualization". This works perfectly for this particular problem and can save you a ton of computational power.

Windowing a list

Windowing a grid (table)

There are many libraries that allow you to use this "windowing" technique, such as react-window and react-virtualized. These two are older compared to react-virtual, which can be added to your project with one simple hook and supports all kinds of customization, including vertical, horizontal, grid, fixed, variable, dynamic, smooth and even infinite virtualization. Definitely give it a try!

Here is a regular data list component.

And here is the windowed version with react-virtual.

Here is a regular data list component.

And here is the windowed version with react-virtual.

Problem: Global state changes trigger slow batch re-renders

Optimizing performance for a few components are relatively easy, compared to doing that for many different components when you use contexts or global state management tools, like Redux, to manage the state of your apps. This scenario doesn't give you a clear clue to fix the problem because none of the components are slow in isolation but they update slow together when there is a global state change. The root of the problem is that there are just too many components responding to the state update and many of them shouldn't even do that.

One way to work around this is to "colocate" the global state down to each component, which works but can be quite some work for you if in a big project. So how can we make every component only re-render when the state it really cares about update?

Solution: Use Recoil

Recoil allow you to connect to the exact piece of state, which is a huge performance benefit. Some people also suggest using MobX, and as the author of MobX Michel Weststrate tweeted, both Recoil and MobX solves the problem of efficient rendering widely shared state, the problem that "React (Context), Redux and most state management libs don't solve." But I would recommend you to use Recoil with React. Because Recoil is built on top of React primitives, which makes it lean, flexible, and integrates closely with React's concurrent mode. It also handles data in an async way in a quasi-functional manner and integrates better with React hooks. In short, Recoil thinks the way that React does. So going with Recoil won't be a bad decision for you if you want to manage complex state performantly with React.

Problem: Monitoring performance in production

Things happen in production, especially for performance problems. There will be situations in which some unexpected mistake slips through the code review process and causes a performance problem. On the other hand, we can't make every user install the React devtool and profile the app for us as they use it. How are we gonna monitor things like that?

Solution: Use the <React.Profiler>

The React team has created an <React.Profiler> API specifically for situation like this. It doesn’t give us quite as much information as the React DevTools do, but it does give us some useful information.

Here is a basic example:

Wrap the target component within the Profiler to enable this feature. It’s important to note that unless you build your app using react-dom/profiling and scheduler/tracing-profiling this component won’t do anything.

For the onRender prop, you'll need to pass a callback function with a particular otherwise the profiler component won't work. The id prop of the Profiler tree that has just committed. You can trace the phase of either "mount" (if the tree just mounted) or "update" (if it re-rendered. actualDuration is the time spent rendering the committed update. baseDuration is the estimated time to render the entire subtree without memoization. startTime is when React began rendering this update. commitTime is when React committed this update. interactions is the Set of interactions belonging to this update. In the callback you can do anything you want with the data, including do calculation, pass the result to your backend, aggregate or log render timings and so on.

Here is a basic example:

Wrap the target component within the Profiler to enable this feature. It’s important to note that unless you build your app using react-dom/profiling and scheduler/tracing-profiling this component won’t do anything.

For the onRender prop, you'll need to pass a callback function with a particular signature otherwise the profiler component won't work. The id prop of the Profiler tree that has just committed. You can trace the phase of either "mount" (if the tree just mounted) or "update" (if it re-rendered. actualDuration is the time spent rendering the committed update. baseDuration is the estimated time to render the entire subtree without memoization. startTime is when React began rendering this update. commitTime is when React committed this update. interactions is the Set of interactions belonging to this update. In the callback you can do anything you want with the data, including do calculation, pass the result to your backend, aggregate or log render timings and so on.

I might write about more things on React performance, such as devtools, React Query, browser performance profiler and so on. Stay tuned.