0% completed
Understanding Memoization in React
On This Page
Saving Results Instead of Repeating Work
A Reusable memoize Function
What Memoization Costs
How This Applies to React
Computing the 30th Fibonacci number with a plain recursive function takes 2,692,537 function calls. The same function with a small cache takes 59 calls, and asking for the same number a second time takes one call. The difference comes from a technique called memoization, and this chapter shows how React uses the same idea to skip work during rendering.
Saving Results Instead of Repeating Work
Here is the recursive Fibonacci function, with a counter that records every call:
let calls = 0; function fib(n) { calls++; if (n < 2) return n; return fib(n - 1) + fib(n - 2); } console.log('fib(30) =', fib(30), 'calls:', calls);
fib(30) = 832040 calls: 2692537
The function is slow because it solves the same smaller problems again and again. fib(30) calls fib(29) and fib(28), but fib(29) also calls fib(28), and each of those repeats the work below it. The number of calls grows by about 1.6 times with every step up in n, and in our runs fib(30) took between 8 and 140 milliseconds, depending on the machine and its load.
The fix is to remember each answer the first time it is computed. Before doing any work, the function checks a Map that stores earlier results; after computing a new result, it saves it there:
const cache = new Map(); let memoCalls = 0; function memoFib(n) { memoCalls++; if (n < 2) return n; if (cache.has(n)) return cache.get(n); const result = memoFib(n - 1) + memoFib(n - 2); cache.set(n, result); return result; } console.log('memoFib(30) =', memoFib(30), 'calls:', memoCalls); memoCalls = 0; console.log('memoFib(30) again =', memoFib(30), 'calls:', memoCalls); console.log('cache size:', cache.size);
memoFib(30) = 832040 calls: 59 memoFib(30) again = 832040 calls: 1 cache size: 29
The memoized version took between 0.05 and 0.16 milliseconds in the same runs.
Saving the result of a function for a given input, and returning the saved result when the same input arrives again, is called memoization. The name comes from "memo", a written note. The saved results are the cache, and returning a saved result is a cache hit.
In the diagram, every gray node of the left tree is work that the right tree does not repeat. A cache helps this much here because the same inputs repeat many times inside one computation. For a function that rarely sees the same input twice, a cache saves almost nothing.
A Reusable memoize Function
The same idea can wrap any function that takes one argument:
function memoize(fn) { const results = new Map(); return (arg) => { if (results.has(arg)) return results.get(arg); const value = fn(arg); results.set(arg, value); return value; }; } let runs = 0; const slowSquare = (n) => { runs++; return n * n; }; const fastSquare = memoize(slowSquare); console.log(fastSquare(9), fastSquare(9), fastSquare(4), 'slowSquare ran', runs, 'times');
81 81 16 slowSquare ran 2 times
What Memoization Costs
Memoization saves time, but it has three costs:
- Memory. Every saved result stays in the cache. The
fibcache holds 29 entries; a cache keyed by user input can grow without limit. - Only pure functions. A function is pure when the same input always gives the same output and it changes nothing outside itself. If a memoized function read the current time or sent a request, the cache would return an old answer.
- Objects are compared by identity. A
Mapfinds an object key only if it is the same object. Two object literals with equal contents are different keys, so the cache misses:
object args: computed 3 times for 4 calls (2 new literals, 1 reused object)
That last point returns in every lesson of this chapter, because React compares props, dependencies and context values in the same way.
How This Applies to React
Rendering a component means calling its function. React calls it again whenever the component's state changes, a context it reads changes, or its parent re-renders. Most of the time this is fast and needs no help. Sometimes a render repeats work whose inputs did not change: a large list is filtered again, or a large subtree is rendered again with the same props.
React offers three tools that apply memoization to rendering. Each one saves a different thing:
| Tool | What it saves | When React reuses it |
|---|---|---|
memo(Component) | A component's last rendered output | Every prop is equal to its previous value |
useMemo(fn, deps) | The result of a calculation inside a component | Every dependency is equal to its previous value |
useCallback(fn, deps) | A function itself, so that it keeps the same identity | Every dependency is equal to its previous value |
"Equal" means the Object.is comparison, which behaves like the Map keys above: equal numbers and strings match, but a new object, array or function never matches the old one.
React Compiler is a build tool that adds this kind of memoization to components and hooks automatically. It is stable and optional: a project turns it on in its build configuration. The lessons in this chapter teach memo, useMemo and useCallback by hand, because most existing code uses them and the compiler applies the same rules. The last lesson of this chapter, "Automatic Memoization with React Compiler", shows what changes when the compiler is enabled.
Reading Progress
0%
On This Page
Saving Results Instead of Repeating Work
A Reusable memoize Function
What Memoization Costs
How This Applies to React