What Is Memoization? Caching Function Results Explained
Memoization caches a function's return value by its input, skipping recomputation on repeat calls. How it works and when it actually helps.
Memoization is an optimization technique that caches the return value of a function based on its input, so that calling the function again with the same input returns the stored result instantly instead of recomputing it. It only works for pure functions — ones where the same input always produces the same output and calling them has no side effects — because the cache is only valid if nothing besides the input affects the result.
A minimal example
Take a naive recursive Fibonacci implementation:
function fib(n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}
This recomputes the same values over and over — fib(5) calls fib(3) twice, fib(2) three times, and so on, with the redundancy growing exponentially as n increases. Memoizing it with a simple cache object fixes that:
function memoize(fn) {
const cache = new Map();
return function (n) {
if (cache.has(n)) return cache.get(n);
const result = fn(n);
cache.set(n, result);
return result;
};
}
const fib = memoize(function (n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
});
Now each distinct value of n is computed exactly once. The recursive calls that would have repeated work instead hit the cache, turning an exponential-time computation into a linear one. This is the same shift in shape that shows up across dynamic programming generally — memoization is dynamic programming’s top-down form, where you let the natural recursion happen but cache along the way instead of building a table bottom-up.
Why purity matters
Memoization silently breaks if the function isn’t pure. Consider a function that depends on the current time, reads from a mutable variable, or makes a network call — caching its result by input alone would return stale or simply wrong data on the next call, because the input isn’t actually the only thing determining the output.
This is the same constraint that makes memoization a natural fit for closures in JavaScript: a memoized function typically wraps its cache in a closure, keeping the cache private to that one memoized instance rather than as shared global state that other code could accidentally mutate.
Cache key design
The example above uses the raw argument as the cache key, which works fine for a single primitive input. Real functions often take multiple arguments or objects, and the key has to capture enough of the input to distinguish genuinely different calls:
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(...args);
cache.set(key, result);
return result;
};
}
JSON.stringify is a common shortcut for multi-argument keys, but it has real limits — it doesn’t distinguish object property order reliably in all engines, can’t key on functions or symbols, and gets slow for large argument objects. Production memoization utilities typically use faster structural hashing or restrict themselves to primitive arguments for this reason. A hash-based cache underneath is conceptually the same idea as a hash table — near-constant-time lookup by key, which is exactly what makes the cache-hit path cheap enough to be worth it.
Time vs memory
Memoization is a textbook time-memory tradeoff: you spend memory holding onto results in exchange for skipping computation later. That tradeoff isn’t free, and it can go the wrong way:
- Unbounded caches leak memory. A memoized function called with an ever-growing set of unique inputs will keep every result forever unless the cache has an eviction policy. Long-running processes — servers, in particular — need a bounded cache (an LRU eviction strategy is the common choice) rather than a plain
Mapthat grows without limit. - Cheap functions aren’t worth memoizing. If the function itself is fast, the overhead of hashing the arguments and checking the cache can exceed the cost of just recomputing the answer. Memoization pays off when the function is expensive relative to a cache lookup — recursive algorithms with overlapping subproblems, expensive parsing, or costly derived calculations.
- Cache invalidation is the hard part. If the function’s true inputs include anything outside its arguments — a database row that can change, a config value — the cache needs a way to become stale intentionally, or it will confidently return wrong answers.
Memoization in frameworks
Front-end frameworks lean on this idea constantly, usually under a different name. React’s useMemo and useCallback hooks memoize a computed value or function reference between renders so that expensive work or child re-renders aren’t repeated when the relevant inputs haven’t changed. The mechanism is the same principle as the Fibonacci example — cache by input, skip recomputation on a match — just applied to render output instead of a return value.
Server-side caching layers apply the same idea at a different scale: instead of caching a function’s return value in process memory, they cache a computed response — a rendered page, an API result — behind a key, often for a limited time. The concept generalizes; see what caching is for how the same tradeoff plays out at the HTTP and infrastructure level rather than inside a single function call.
The takeaway
Memoization caches a pure function’s output by its input, trading memory for time on repeat calls. It turns exponential recursive algorithms with overlapping subproblems into linear ones, and it underlies patterns from useMemo in React to top-down dynamic programming. The catch is purity and cache growth: memoize a function with side effects or unpredictable output and you’ll get wrong answers back with confidence, and memoize without an eviction strategy in a long-running process and you’ll trade a CPU problem for a memory one.
Keep reading
The Lycoris Team · · 4 min read What Is a Monad? A Practical Explanation for Programmers
A monad is a wrapper type with rules for chaining operations that might fail, be async, or carry extra context — like Promise or Optional, generalized.
Takina · · 4 min read TypeScript Abstract Classes, Explained
Abstract classes in TypeScript define shared implementation plus methods subclasses must fill in. How they differ from interfaces and when to reach for them.
Takina · · 3 min read React Suspense, Explained
React Suspense lets components pause rendering while they wait on async data, showing a fallback UI instead of manual loading-state juggling.