Performance optimization with useMemo
One of those things that you don't know enough, and then when you know somewhat you tend to use a bit too much is React beloved and behated useMemo hook (Opens in a new tab). In my quest of trying to understand more the features of React that I use on a regular basis, I find myself in need to write more about this hook, clarifying what is it, when to use it and when not.
So, let's get started and unravel this madness!
What is useMemo?
Introduced in React 16.8, the useMemo hook is a built-in feature designed to memoize values and prevent unnecessary recalculations and re-renders, meaning that instead of calculating things on the fly, it will first check if that computation has already done and take the value already stored.
... Wait wait wait! memoization what?! What's that?!
In programming, memoization is an optimization technique that makes applications more efficient and hence faster. It does this by storing computation results in cache, and retrieving that same information from the cache the next time it's needed instead of computing it again.
In simpler words, it consists of storing in cache the output of a function, and making the function check if each required computation is in the cache before computing it.
So far so good. Yes?! Nope.
I have heard that useMemo should not always be used and this explanation makes me think instead that it always sounds like a good idea, so let's continue digging up a bit more more, but let's go the long route and let's see first how to implement it.
How to use useMemo
The basic syntax for useMemo is
This line of code contains the function used to calculate the value and the dependency array. For example, consider a component that filters a list of items based on a search term:
In this example, we use useMemo to memoize the filtered list of items and avoid recalculating the filtered list every time the component renders. Still sounds pretty good and still like I should actually be using it more than I actually do. 🤔
Browsing various examples online, as you can see from the first snippet of code in this article, however, I find the emerging written pattern of using useMemo only for expensive calculations, computed expensive value, expensive operations, and I finally start understanding that there is something particularly in mind for which useMemo is recommended.
Expensive calculations are tasks that require a significant amount of processing time and resources: they might be time-consuming, memory-intensive, or computationally complex. What seems to fit in this category is:
- Manipulating large strings, such as parsing and transforming text
- Transforming large datasets through iteration, while performing multiple calculations on the data
- Sorting or filtering large arrays
- Fetching data through APIs/databases
- Processing and manipulating big files
- Rendering a top component with a significant number of nested components and elements
- Recursive algorithms, advanced statistical calculations, matrices, weird formulas etc. etc. etc.
React useHook official documentation (Opens in a new tab) has a section How to tell if a calculation is expensive? (Opens in a new tab) where it teaches us to use the console by adopting
console.timeEnd()to better determine the cost of our operations.
Not all these operations are inherently worthy of useMemo though! There are at least a few things to consider.
1. How often is the operation performed?
If the operation is performed frequently or triggered by state/props changes, and the result doesn't change as often, memoizing the result can prevent redundant recalculations, therefore improving overall performance.
For example, if you have a big array of strings that needs to be formatted in a particular way, and you might want to list all of them in a nice HTML list, using useMemo might help avoiding slow renders in the UI.
2. How reusable is the result of useMemo?
The result is used within the component or passed down as props to child components; if the result is used multiple times, useMemo can indeed help avoid unnecessary recalculation.
Think of a large file that you want to use in different sections of your app; it's definitely worth contemplating memoizing it.
3. Is it worth the memory consumption?
As the memoized values are stored in memory until the component unmounts or the dependencies change, memoizing large datasets will definitely increase memory consumption; if the fetching from caching is faster than the recomputation of the calculations, perhaps useMemo might have a valid purpose.
If you're fetching a big list and your interface freezes, perhaps yes, it's time to think about useMemo.
In all of the above cases, and even more critical than for
useEffect, the dependencies of the memoized function need to be properly evaluated to make sure that useMemo recalculates the memoized value only when necessary, as performance might otherwise ironically drop.
In his article Understand when to use useMemo (Opens in a new tab), Max Rozen (Opens in a new tab) puts it up in simple words:
Don't use useMemo until you notice parts of your app are frustratingly slow. Premature optimisation is the root of all evil, and throwing useMemo everywhere is premature optimisation.
"Premature optimization is the root of all evil"
This paragraph might be a tad too long and expanding on a topic that is not quite the purpose of this article; feel free to move to the next header if you already have a grasp about premature optimization.
Calling premature optimization the root of all evil seems to be something rather common, and Dr. Itamar Shatz (Opens in a new tab) in his article about Premature Optimization: Why It’s the “Root of All Evil” and How to Avoid It (Opens in a new tab) says that
Premature optimization involves trying to improve something — especially with the goal of perfecting it — when it’s too early to do so.
His article also shows that the famous Premature optimization is the root of all evil sentence is a quote from the computer scientist Donald Knuth (Opens in a new tab), from his article Structured Programming with go to Statements (Opens in a new tab). The article is a reminder to developers not to spend too much time or resources optimizing parts of a program before they need to and especially before they have a working version.
According to Donald Knuth, premature optimization leads to wasted effort, as you might end up optimizing parts of the code that don't have a significant impact on the overall performance, or even optimizing code that eventually gets removed or significantly changed.
His recommendations are to first build a functional system, then identify any performance issues, and finally optimize critical parts that actually need optimization, without forgetting that readability and maintainability are often more important than minor efficiency gains, especially in early development stages.
Dr. Itamar Shatz lists out the dangers of premature optimization (Opens in a new tab) in a few yet impactful points, which I recommend reading from his article.
I have now some ideas on where useMemo could come at hand as well as potential dangers of premature optimization, but I still feel like the constraints for which it's recommended are still a bit unable to give a clear line. Perhaps it'd be beneficial to go the other route and analyze when not to use useMemo.
When not to use useMemo
By what we've figured out before, if an operation is relatively simple, doesn't consume much processing time, and doesn't depend on frequently changing data, using useMemo will add unnecessary overhead without significant performance gains.
- if a component doesn't have any expensive calculations and doesn't render frequently
- or the expensive operation is isolated to a specific part of the component that doesn't impact the overall rendering performance
- or the expensive operation varies significantly between the instances where the component is used
- and we're trying to accommodate all sorts of devices that might not have a lot of memory available
- and the performance improvement is not noticeable by end-users
... we might find ourselves perhaps not in need of useMemo.
We also need to consider that adding useMemo to memoize some calculations can make writing unit tests more complicated, and while it's not a reason to avoid useMemo, it's definitely worth to weigh the benefits of memoization also against the ease of testing and maintaining the code.
From all this reading and writing, I can deduct that there are plausible usages where you expect to process a lot of data, that won't change as often, such as:
Another plausible example would be:
... And potentially, there could be so many bad/unrecommended implementations where there's no big computation needed and therefore useMemo is bringing no real benefit to the end user:
Ultimately, the trend with useMemo seems to be that as soon as people get to know more about it, they want to implement it, whereas it'd make more sense to leave it for only when the app clearly shows benefits from implementing it. It sounds like a pretty good news to me as it means that most likely I don't have to change anything in this website as I don't seem to see anything particularly too slow... yay! 😂
I've truly enjoyed writing about this and I can't help but recommend strongly to read the sources/resources attached to the article; if you feel like I've miswritten something (which can very well be the cause as my best corrections often come days after I rewrote something), don't hesitate to mention that in the comments below!
But before I leave...
A brief note about useMemo vs. useCallback
You'll also hear this one too! And while
useCallback is gonna be a topic for another day, let's just iron this one last thing out before we call it a day!
useCallback is similar to useMemo, but instead of memoizing the result of a function, it memoizes the actual function itself, which is particularly useful when you have a component that receives a function as a prop and you want to prevent unnecessary re-rendering of child components that depend on that function. This means that if the dependencies of useCallback don't change, the same function instance is used, avoiding unnecessary re-renders of child components that depend on it.
Now out! Bye! :D