Improving React application performance: React.memo vs useMemo
As a React developer, you’ve most likely come across situations where optimizing the performance of a component is necessary. There are certain cases where rendering a component might be so expensive that it uses up a lot of CPU resources and/or memory (i.e rendering a graph with thousands of data points).
When this happens, there are some React APIs that we can reach for to improve performance and today, we’ll focus on two of the most misunderstood ones — React.memo and useMemo.
Before we do this, however, it’s important to get a clear understanding of the concept of memoization and how it relates to these two APIs.
What is memoization?
Memoization is a fancy computer science buzzword that just means a way to avoid doing unnecessary work. Wikipedia describes memoization as —
“In computing, memoization or memoisation is an optimization technique used primarily to speed up computer programs by storing the results of expensive function calls and returning the cached result when the same inputs occur again.”
Let’s think about it this way. Say you were taking a Mathematics quiz and you were asked the products of 99 and 22. The first time you hear the question, it might take you some seconds to perform the calculations and you arrive at the answer 2,178.
However, if at a later stage you were asked the same question of 99 * 22, you already know the result from the last time you answered it and you can answer the question immediately without having to do the calculations all over again.
In essence, you memoized (memorized) the result of 99 * 22 the first time you calculated it and every time someone asks you what the answer is, you no longer have to calculate it again. You already know the answer.
This is exactly how memoization works in programming as well. If I had an unoptimized function that calculated the digits of PI, it would consume a lot of CPU resources every time I called this function.
function digitsOfPI(numOfDigits) { ...some logic }
// First call, we use up a lot of resources to calculate the answer
digitsOfPI(100)
But if the result from this function was memoized, then it would only use up CPU resources the first time it was called. If you called it a second time and asked for the same number of digits as before, the function can just return the memoized result without having to calculate anything again.
// Second call, we've calculated 100 digits of PI before
// so the function already knows the answer and doesn't use
// up any resources calculating it.
digitsOfPI(100)
It is important to note, however, that memoization is not free and in some cases can actually lead to worse performance. We’ll explore this in more detail below.
React.memo
How does this relate to React? It becomes useful when you consider how React updates the DOM. When updating the DOM, React renders a component and compares the current and previous render. If they are different, it updates the DOM with the new render, and if they aren’t, it just discards the current render.
But notice that for React to decide whether to update the DOM, it first has to render the component. And if doing so is expensive, you want to minimize the number of times it happens. You can do this by bypassing unnecessary renders using React.memo
.
If you wrap a component in React.memo
, React will still render the component normally the first time but it also remembers the props. Before the next render, React will compare the new props and the old ones. If they are the same, it just returns the previous result instead of rendering it again.
Example use case of React.memo Take this component which renders a bar graph based on data it receives from props.
const BarGraph = ({ data }) => {
...some expensive calculations
...render method
}
This component will always render the same graph given the same dataset while performing some expensive calculations (expensive here might mean it takes 400ms to process the data). So we only want it to render when the data
prop changes.
However, the data prop is most likely an array or object so React will always see it as a changed prop between renders even when it hasn’t really changed. This is because of how object equality works in JavaScript. An object is only equal to another object if they point to the same location in memory regardless of the actual contents. (Learn more about object equality on MDN).
This means React will waste some time rendering it and comparing the result before seeing that the result is the same and discarding the render. This is not ideal because it’s an unnecessary cycle and more importantly, an expensive one too.
We can fix this by telling React not to render the component unless the actual value of the data
prop changes using React.memo like so —
export const memoisedBarGraph = React.memo(BarGraph)
Ideally, this should fix the issue but in this case, it does not. React.memo uses a shallow comparison of the component props and because of how JavaScript works, comparing objects shallowly will always return false even if they have the same values.
This is why React.memo also takes in a second argument. This argument is a custom equality check function that you, the developer, uses to decide if the component should render (Think shouldComponentUpdate).
The solution then, for our initial problem would look something like this —
const barGraphPropsAreEqual = (prevProps, newProps) => {
...some custom logic to determine if the props are equal
}
// we update our React.memo usage
export const memoisedBarGraph = React.memo(BarGraph, barGraphPropsAreEqual)
Now, when a render is triggered (i.e the parent component rerenders), React.memo can use our custom equality check function to determine if the props for the BarGraph component have changed and if it should rerender.
If our function returns true, React bails out of the render and doesn’t update the DOM. If it returns false, React renders the component again and updates the DOM.
When to use React.memo You can use React.memo if your component —
- Will always render the same thing given the same props (i.e, if you have to make a network call to fetch some data and there’s a chance the data might not be the same, do not use it)
- Renders expensively (i.e, it takes at least 100ms to render)
- Renders often
If your component does not meet the above requirements, then you might not need to memoize it as using React.memo can, in some cases, make performance worse as it’s more code for the client to parse.
When in doubt, profile your component to see if it would be beneficial to memoize it.
Potential performance pitfalls There’s a reason why React.memo utilizes shallow comparison by default for determining when to rerender. This is because there is an additional overhead in checking if a value has been memoized every time we need to access it and the more complex the data structure of the value, the worse the overhead is.
For strings, numbers, and dates, comparing the values is trivial but for objects, it can get very resource-intensive based on how complex the object is.
So if a component renders often but the custom equality check function is more expensive, then the performance gets worse instead of it getting better. This is why it is important to benchmark a component before and after using React.memo to ensure that it is actually worth memoizing.
Callback functions Before springing for React.memo, it is important to check if the component accepts any callback functions. This is because a function in JavaScript is only equal to its instance. What does this mean?
If I pass in a prop to a component like this —
<BarGraph
data={data}
onRenderFinish={() => showNotification(subscribedListeners)}
/>
onRenderFinish
will have a different instance on each render and if you don’t provide a custom equality check function, then React.memo will re-render everytime the function instance changes even if it is basically the same function. This can lead to worse performance as React will have more work to do when rendering the component — first doing a props comparison, and then rendering anyway because the callback prop has a different instance every time.
There are multiple ways to fix this issue but the officially recommended way is to wrap the callback prop in a useCallback hook like so —
const onBarGraphRenderFinish = useCallback(
() => showNotification(subscribedListeners),
[subscribedListeners]
);
<BarGraph
data={data}
onRenderFinish={onBarGraphRenderFinish}
/>
Learn more about the useCallback hook.
Another way to fix this is through the custom comparison function passed to React.memo. You can convert the function prop to a string and compare the string values but this comes with some caveats.
const barGraphPropsAreEqual = (prevProps, newProps) => {
...some custom logic to determine if the props are equal
if (typeof prevProps[propKey] === 'function') {
const isEqual = prevProps[propKey].toString() === newProps[propKey]
}
This could also be an expensive operation depending on the size of the function but in most cases, should work fine.
You could also decide not to compare the callback function in the custom equality check function since you can decide which props trigger a re-render. But there are cases where it is actually important to compare a callback function prop.
In our case, if subscribedListeners
changes and you ignore the onRenderFinish
prop in the equality check function, you’ll effectively be showing notifications to an outdated list of subscribers.
However, If you do not have any values in the callback that could change over the course of time, then ignoring it in the equality check function would be fine.
useMemo
useMemo is one of the built-in hooks in React and it performs a fundamentally similar but different job to React.memo. Similar in the sense that it also memoizes values but different because useMemo
is a hook and as a result is limited in how it can be used.
Where we used React.memo to optimize a graph component, we can’t use useMemo
for the same purpose. Instead, it should be used in cases where a derived value is expensive to calculate on every render. What does this mean?
Example use case of useMemo Say we have a component that takes in some data and encrypts it using a string key from props like so —
const Encrypter = ({ dataToEncrypt, encryptKey }) => {
const encryptedData = encrypt(dataToEncrypt, secretKey);
...render method
}
Whenever this component renders, it has to encrypt the data passed to it every time regardless of if the props change. If the encrypt
function takes a long time to do this, we might want to reduce the number of times we have to run it.
This is where useMemo
shines. If we have a variable inside a component that is expensive to compute, we can use useMemo
to reduce the number of times this has to happen by memoizing the computed variable.
In our example case above, this would look something like —
const Encrypter = ({ dataToEncrypt, encryptKey }) => {
const encryptedData = useMemo(
() => encrypt(dataToEncrypt, secretKey),
[dataToEncrypt, encryptKey]
);
...render method
}
Now, every time the Encrypter
component renders, before running the expensive encrypt
function, useMemo
checks to see if the dataToEncrypt
and encryptKey
values have changed between renders.
If not, it simply returns the result of the previous render without calling the encrypt
function. If it changed, then we calculate the result again.
Instead of a custom equality check function as in React.memo, useMemo
takes in a dependency array (i.e the values that need to change for the result to be calculated again) to determine when to “forget” the previous result and recalculate it (I’m not a 100% sure if React only memoizes the previous render or if there’s an internal cache of previously memoized renders).
If the values in the dependency array are objects or functions, useMemo
checks for referential equality to determine whether to recalculate the result. This means that for an object or function value, React only considers the value changed if the reference in memory has changed regardless of the actual contents.
In many cases, the object and function instances (except arrow functions because they’re always recreated on each render) don’t change between renders but in cases where deep comparison is necessary, you can use a custom hook like useMemoCompare.
When to use useMemo
Before deciding to use useMemo
, make sure that —
- You have profiled the component and determined if it’s calculating an expensive value in every render
- There are no side effects (e.g async calls) in your
useMemo
hook because it runs during rendering and all side effects belongs in the useEffect hook. - You do not break the rules of React Hooks
Which one should I pick?
Now that we have a solid understanding of both React.memo
and useMemo
, deciding which one to use should be simple.
- If you’re memoizing a whole component, use
React.memo
- If you’re memoizing a value inside a functional component, use
useMemo
It’s noteworthy to mention again that before you spring for either of these solutions, ensure that your use case warrants it as if used carelessly, they could harm performance instead of improving it.
Measuring front-end performance
Finally, it’s important to understand that monitoring the performance of a web application in production may be challenging and time consuming. OpenReplay is an open-source session replay stack for developers. It helps you replay everything your users do and shows how your app behaves for every issue. It’s like having your browser’s inspector open while looking over your user’s shoulder.
OpenReplay lets you reproduce issues, aggregate JS errors and monitor your app’s performance. OpenReplay offers plugins for capturing the state of your Redux or VueX store and for inspecting Fetch requests and GraphQL queries.
Happy debugging, for modern frontend teams - Start monitoring your web app for free.