Memoization
Memoization is a fancy word for a simple idea: remember what a function returned for the arguments it received, and on the next call with the same arguments, return the cached answer instead of computing it again. It is one of the cleanest examples of trading memory for time, and the implementation in JavaScript is a one-screen function.
A simple memoize
function memoize(fn) {
const cache = new Map();
return function (...args) {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}A Map holds previously-seen (args, result) pairs. The key is a stringified version of the arguments — fine for primitives, less ideal for objects (more on that below).
A worked example
function slowSquare(n) {
console.log("computing", n);
for (let i = 0; i < 1e7; i++) {} // pretend it is slow
return n * n;
}
const fastSquare = memoize(slowSquare);
fastSquare(4); // logs "computing 4", returns 16
fastSquare(4); // no log — cached, returns 16 instantly
fastSquare(5); // logs "computing 5", returns 25
fastSquare(4); // still cachedcomputing 4 computing 5
Recursive memoization
The textbook case is Fibonacci. The naive version is exponential because each call re-computes the same sub-problems.
// Without memo — O(2^n).
function fib(n) {
return n < 2 ? n : fib(n - 1) + fib(n - 2);
}
// With memo — O(n).
const memoFib = memoize(function inner(n) {
return n < 2 ? n : memoFib(n - 1) + memoFib(n - 2);
});
memoFib(40); // instantBetter keys for object arguments
JSON.stringify is fast and easy, but two objects with the same content stringify to the same key — which is usually what you want, but not always. For reference-based caching (one entry per exact object), use a WeakMap.
function memoizeByRef(fn) {
const cache = new WeakMap();
return function (obj) {
if (cache.has(obj)) return cache.get(obj);
const result = fn(obj);
cache.set(obj, result);
return result;
};
}
const stats = memoizeByRef(user => ({ total: user.orders.length }));WeakMap keys are held weakly — when the object is garbage-collected, the cache entry disappears automatically. No memory leak.
When does memoization pay off?
Deterministic functions — same input must always produce the same output. Otherwise the cache lies.
Expensive to compute — parsing, layout maths, regex compilation, JSON traversal.
Called with repeating inputs — if every call uses a fresh argument, the cache only grows.
Pure — no side effects. Cached calls would skip those effects.
Cache eviction
The simple Map cache grows forever. For long-lived processes you usually want a bound — an LRU (least-recently-used) cache discards the entry that was used least recently when the cache fills.
function memoizeLRU(fn, max = 100) {
const cache = new Map();
return function (...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
const value = cache.get(key);
cache.delete(key); // re-insert -> becomes most-recent
cache.set(key, value);
return value;
}
const value = fn.apply(this, args);
cache.set(key, value);
if (cache.size > max) {
const oldest = cache.keys().next().value;
cache.delete(oldest);
}
return value;
};
}React useMemo
React's useMemo and useCallback are runtime memoization scoped to a component. They cache the latest value, not every past value — the cache size is always 1.
// Pseudocode — recompute only if deps change. const result = useMemo(() => expensive(items), [items]);
Things to remember
Cache only pure functions. Anything with side effects or time-dependence will misbehave.
Pick a key strategy that matches your inputs. Primitives →
JSON.stringify. Objects → reference (WeakMap) or a stable id.Decide whether the cache should be unbounded, sized, or time-based. Unbounded is fine for a short script and a leak in a server.