Performance optimization with useMemo

Lucia Nazzaro
Lucia Nazzaro
Lead Frontend Developer
Published
Reviewed
81 views

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. 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?!

Germán Cocca makes a great job at explaining this concept in his article about Memoization in Javascript and React, and I will not try to rewrite it when he's already told it better than I ever can!

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

1const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

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:

1import React, { useMemo } from 'react'; 2 3const FilteredList = ({ items, searchTerm }) => { 4 const filteredItems = useMemo(() => { 5 return items.filter(item => item.toLowerCase().includes(searchTerm.toLowerCase())); 6 }, [items, searchTerm]); 7 8 return ( 9 <ul> 10 {filteredItems.map(item => ( 11 <li key={item}>{item}</li> 12 ))} 13 </ul> 14 ); 15}; 16

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.

But what is an expensive calculation in JavaScript?

Expensive calculations

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 has a section How to tell if a calculation is expensive? where it teaches us to use the console by adopting console.time() and 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, Max Rozen 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 in his article about Premature Optimization: Why It’s the “Root of All Evil” and How to Avoid It 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, from his article Structured Programming with go to Statements. 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 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.

Therefore

  • 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.

Example implementations

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:

1// Plausible usage of useMemo 2import React, { useMemo } from 'react'; 3 4const BigProcessedList = ({ data }) => { 5 const processedData = useMemo(() => { 6 return data.map((item) => { 7 // Simulating an expensive computation 8 for (let i = 0; i < 1e6; i++) {} 9 return item * 2; 10 }); 11 }, [data]); 12 13 return ( 14 <ol> 15 {processedData.map((item, index) => ( 16 <li key={`${index}__${item}`}>{item}</li> 17 ))} 18 </ol> 19 ); 20}; 21

Another plausible example would be:

1// Plausible good use of useMemo 2import React, { useMemo } from 'react'; 3 4const EmployeeList = ({ employees, filter }) => { 5 const filteredEmployees = useMemo(() => { 6 return employees.filter(emp => emp.department === filter); 7 }, [employees, filter]); 8 9 return ( 10 <ul> 11 {filteredEmployees.map((employee) => ( 12 <li key={employee.id}>{employee.name}</li> 13 ))} 14 </ul> 15 ); 16};

... 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:

1// Unoptimal use of useMemo 2import React, { useMemo } from 'react'; 3 4const CombinedStringComponent = ({ propA, propB }) => { 5 const combinedProps = useMemo(() => { 6 return `${propA} ${propB}`; 7 }, [propA, propB]); 8 9 return ( 10 <div>{combinedProps}</div> 11 ); 12};

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

This article was published also in




More Articles

Level up your Tailwind game

Use Tailwind like you mean it! Whether you're a seasoned developer or just starting out, this article will help you navigate Tailwind CSS and expand your knowledge with advanced practices.

430
TailwindCSS
+1

Fixing Disqus 'Auto' theme switching when using Next.js + next-themes

So, you want to have both a light and dark theme in your Next.js website, but your Disqus embed doesn't seem to change its theme accordingly... Maybe this will help!

223
ReactNext.js
+1