Improving React application performance: React.memo vs useMemo
Memoisation sounds fancy but it is really just saving a value and using it later. If you're searching for new ways to boost your React app performance then this article might be for you.
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.
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.
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.
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
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.
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.
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 —
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 —
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 —
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.
If I pass in a prop to a component like this —
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 —
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.
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 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 —
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 —
Now, every time the
Encrypter component renders, before running the expensive
useMemo checks to see if the
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 —
useMemohook because it runs during rendering and all side effects belongs in the useEffect hook.
Now that we have a solid understanding of both
useMemo, deciding which one to use should be simple.
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.