Skip to content

Edvins Antonovs

useMemo overdose

Recently, I've been asked when to use the useMemo hook, and this question made me think and reflect on it. I slowly realised that I fell into the habit of using the useMemo hook for pretty much everything, and I couldn't explain why I was doing it. And especially what made me feel worried is that after a chat with another front-end engineer, I've realised I'm not the only one doing it. It means that developers tend to overuse the useMemo hook and can't even adequately explain why they are doing it. In this post, we will learn when to use the useMemo hook and when not.


tl;dr

  • Profile first, optimise second. Before optimising a component, use React's built-in tools to accurately profile performance and identify bottlenecks.

  • Memoise only expensive computations. Apply useMemo only to genuinely resource-intensive computations that occur frequently.

  • Keep dependencies accurate. Double-check that all relevant dependencies are provided correctly to useMemo to ensure it updates when needed.


Purpose of useMemo hook

Before delving into the overuse of useMemo, let's briefly understand its purpose. The useMemo hook is used to memoise the results of a function and returns a cached value that only changes when its dependencies change. This ensures that expensive computations are not repeated on every render, thus optimising the performance of React components.


When to use the useMemo hook

useMemo should be employed when dealing with computations that are time-consuming or resource-intensive, and the results don't change between renders. For instance, calculating the factorial of a number or formatting large datasets for display are suitable scenarios for using useMemo.


Pitfalls of useMemo hook

After reflecting about my overuse of useMemo hook, I realised that there are a few pitfalls that we should be aware of when using useMemo hook.

  • Premature optimisation. One common mistake is prematurely optimising components with useMemo before identifying actual performance bottlenecks. It's essential to profile your application and identify which parts are genuinely causing performance issues before adding memoisation.
  • Unintended side effects. Overusing useMemo can lead to unexpected side effects if dependencies are not correctly managed. If dependencies are accurately provided, you might avoid situations where the cached value does not update when it should, leading to outdated or incorrect results.
  • Unnecessary complexity. Excessive memoisation can clutter the codebase and make it easier to understand, especially for developers who need to become more familiar with memoisation. It can obscure the actual logic of the component, making it difficult to maintain and debug.

What is considered an expensive computations?

Okay, expensive computations were mentioned multiple times, but how can I understand what is considered expensive? Well, it's hard to say, depending on the context. For instance, if you are working with a large dataset and need to format it for display, this is considered an expensive computation. However, if you are working with a small dataset, then this is not considered an expensive computation. In other words, if the computation takes a long time to complete, it's considered expensive.

  • Nested loop. Computations that involve multiple nested loops, especially when operating on large datasets, can be expensive.
  • Recursive operations. If not optimised carefully, recursive algorithms can lead to exponential time complexity.
  • Heavy data transformations. Data manipulations like filtering, mapping, or sorting large arrays can be resource-intensive. This is especially true if the operations are chained together.
  • Complex mathematical calculations. Mathematical computations that involve complex operations or iterative processes may take considerable time.

Preview

Let's take a look at the following example. We have NonMemoComponent and MemoComponent; in both, we use performance.now() to measure the execution time of the expensiveCalculation function. The only difference between these two components is that MemoComponent uses the useMemo hook to memoise the result of the expensiveCalculation function. Let's see what happens when we click the button and increment the count in both cases.

NonMemoComponent.jsx
1import React, { useState } from 'react';
2
3const NonMemoComponent = () => {
4 const [count, setCount] = useState(0);
5
6 const expensiveCalculation = () => {
7 let result = 0;
8 for (let i = 0; i < 10000000; i++) {
9 result += i;
10 }
11 return result;
12 };
13
14 const handleClick = () => {
15 setCount(count + 1);
16 };
17
18 const startTime = performance.now();
19 const result = expensiveCalculation();
20 const endTime = performance.now();
21
22 console.log('Non-memoised calculation execution time:', endTime - startTime);
23
24 return (
25 <div>
26 <button onClick={handleClick}>Increment Count</button>
27 <p>Count: {count}</p>
28 <p>Result: {result}</p>
29 </div>
30 );
31};
32
33export default NonMemoComponent;

MemoComponent.jsx
1import React, { useState, useMemo } from "react";
2
3const MemoComponent = () => {
4 const [count, setCount] = useState(0);
5
6 const expensiveCalculation = useMemo(() => {
7 const startTime = performance.now();
8 // Simulate an expensive calculation
9 let result = 0;
10 for (let i = 0; i < 10000000; i++) {
11 result += i;
12 }
13 const endTime = performance.now();
14
15 console.log("Memoised calculation execution time:", endTime - startTime);
16
17 return result;
18 }, []); // Include 'count' as a dependency if you want to recompute when it changes.
19
20 const handleClick = () => {
21 setCount(count + 1);
22 };
23
24 return (
25 <div>
26 <button onClick={handleClick}>Increment Count</button>
27 <p>Count: {count}</p>
28 <p>Result: {expensiveCalculation}</p>
29 </div>
30 );
31};
32
33export default MemoComponent;

I've used CodeSandbox for this snippet, but to run this example, you must disable Infinite Loop Protection in sandbox.config.json.

If you open the console, you can see that Memo execution time: 0. How come? You're seeing this in the console for the first render of the memoised component because of how the code is structured.

In the memoised component, the expensiveCalculation is wrapped in a useMemo hook with an empty dependency array. This means the calculation is performed only once when the component is initially rendered, and the result is memoised (cached) for subsequent renders. Since the calculation is done during the initial render, the memoised result is immediately available and doesn't require an additional execution time when you log in.

Here's a breakdown of what's happening:

  1. The memoised component is rendered for the first time.
  2. The useMemo hook calculates the result of expensiveCalculation during the initial render.
  3. The calculated result is immediately available without additional computation.
  4. When you log the result, it appears as if the execution time is 0 because the computation already happened during the initial render.

This is a characteristic of how useMemo works. It's designed to optimise expensive computations by calculating them only when necessary and then reusing the cached result for subsequent renders as long as the dependencies remain the same. In this case, the empty dependency array ensures that the calculation is done only once during the initial render.

© 2024 by Edvins Antonovs. All rights reserved.