Skip to the content.

Memoization is just caching with a key you have to get right

Memoization is just caching with a key you have to get right

Memoization is caching for function calls: remember the result of a call, keyed by its inputs, and return the remembered result when the same inputs come back instead of recomputing. That is the whole idea, and it is genuinely simple. The part that trips everyone is the key — memoization only works if you can decide, cheaply and correctly, whether the inputs are “the same.” Get the comparison wrong and the cache either never hits (so the memo does nothing) or hits when it shouldn’t (so you serve a stale answer). Most broken memoization is a broken key.

A memoized call: hit returns the cache, miss computes and stores A call arrives with a key. If the key matches the cached key it is a hit and returns the stored value. If not it is a miss, computes the result, stores it against the new key, and returns it. call(key) samekey? hit return cache miss compute + store
Every memo is this branch: a matching key returns the stored value; a new key pays the full cost once and remembers it.

A memo is a closure over a cache

At its simplest, memoization is a wrapper that keeps a map from a serialised key to a result. This works when the arguments serialise cleanly to a string:

function memoize(fn) {
  const cache = new Map();
  return (...args) => {
    const key = JSON.stringify(args);       // the key: get this right or nothing works
    if (cache.has(key)) return cache.get(key);
    const result = fn(...args);
    cache.set(key, result);
    return result;
  };
}

const slowSquare = memoize((n) => { /* expensive */ return n * n; });
slowSquare(9);  // computes
slowSquare(9);  // cache hit — no recompute

Reference equality is the key most frameworks use

JSON.stringify is fine for primitives but wrong for the hot path in a UI, where inputs are objects and arrays. React’s useMemo and Redux’s reselect compare inputs by reference (===), not by value — which is fast but means a new object with the same contents counts as a change and busts the cache:

// BROKEN: `config` is a new object every render, so the memo never hits
const value = useMemo(() => compute(config), [{ mode: "fast" }]);

// FIXED: a stable reference — same object across renders, so the key holds
const config = useRef({ mode: "fast" }).current;
const value = useMemo(() => compute(config), [config]);

This is the number-one reason a useMemo “does nothing”: one of its dependencies is freshly allocated on every render, so the key never matches and it recomputes every time — you pay the memo’s overhead and the full cost.

Memoize the expensive and the shared, not everything

Memoization is not free: it holds memory and adds a comparison on every call. It pays off for genuinely expensive computations, and for derived values shared by many consumers (a selector feeding twenty components), where one recompute saves twenty. It is waste on a cheap function called once — the comparison and the retained reference cost more than the arithmetic you were trying to skip, and now the garbage collector has more to track for no benefit. Worse, an unbounded cache is a memory leak: a Map keyed on an ever-changing input grows forever unless you cap it or evict old entries, so a memo on a high-cardinality key can quietly consume the heap. The reselect pattern — compose small memoized selectors so a change to one slice doesn’t recompute another’s derived data — is where this earns its keep, and it is exactly what the memoized-selector exercise builds: correct keys, reference-stable inputs, and a cache that actually hits.