PerformanceGENERALFRAMEWORK-SPECIFICDEVICE-SPECIFIC

Memoization

Trading recomputation for memory and an invalidation problem. Sometimes clearly worth it; applied everywhere, a net loss with extra bugs.

The intent, the obvious build, and why it breaks

Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.

The question

Should I memoize this, and how would I know?

The user intent

Someone types in a filter box and expects each keystroke to appear immediately, not a word behind.

The obvious build

Recomputation is waste, so memoize everything. It cannot make things slower.

Why it breaks

It can. Every memoized value costs a cache entry, a dependency comparison on each evaluation, and retained memory for as long as it lives.

How it breaks in a real browser
  • It can. Every memoized value costs a cache entry, a dependency comparison on each evaluation, and retained memory for as long as it lives.
  • For a cheap computation the comparison costs more than recomputing — you have added work and called it an optimisation.
  • Retaining results keeps their inputs alive, which turns a memo into a leak when the key space is unbounded (Memory Leaks).
  • A wrong dependency list is a correctness bug, not a performance one: the value goes stale and the UI shows something that is no longer true.
  • Memoizing everything makes the genuinely expensive computation invisible among hundreds of trivial ones, so the real cost never gets found (Measure Before Optimising).
IntentEventStateUI LogicDOM WorkNetworkLayout / PaintPixelsFeedback

What is actually happening

In the browser, not in the framework.

  • Memoization stores a result keyed by its inputs and returns the stored value when the inputs are unchanged. Three costs follow immediately: storage, comparison, and invalidation.
  • Comparison is usually shallow reference equality, which is fast and means a newly-created object or array — even with identical contents — counts as changed. This is why a memo that "never hits" is almost always a new object being created upstream on every render.
  • In component frameworks there are two distinct things called memoization: caching a computed value, and skipping a re-render. They have different costs and different failure modes, and conflating them is common (What a Component Costs to Render).
  • A skipped re-render only helps if the render was expensive. A component whose render is trivial gains nothing and pays the comparison (Reactivity Models).
  • The correctness requirement is that the dependency list is complete. If a value the computation reads is not in it, the memo returns a stale answer confidently.
  • For a genuinely expensive pure computation over stable inputs, memoization is straightforwardly correct and valuable — that case exists and is worth recognising.

What this makes the browser do

And which of it is avoidable.

  • Comparison on every evaluation, proportional to dependency count.
  • Memory retained for every cached result, for the lifetime of whatever holds the cache.
  • The avoided work, when it hits — which is the entire benefit and is often assumed rather than measured.
  • Additional collection pressure from retained results, which lands as longer pauses on the main thread (Memory Leaks).

What it actually costs

The reason "memoize everything" is wrong is arithmetic. A memo is worth it when the work avoided exceeds the comparison plus the retention, weighted by how often it actually hits. Two of those three terms are usually assumed.

The hit-rate term is the one that catches people out, and it is almost always the same cause: an input recreated on every render, so the comparison always says "changed" and the cache never returns anything.

Should this be memoized?

What did the profile show?

Do not memoize

when The computation is cheap, or you have not measured it.

cost None. This is the default, and most code should stay here (Measure Before Optimising).

Remove the work instead

when The value is derived and does not need recomputing, or the computation can move out of the render path.

cost Restructuring — and strictly better than caching, because there is nothing to invalidate (Derived State).

Stabilise the input first

when A memo exists and never hits.

cost Usually a one-line fix upstream; try this before adding another memo.

Memoize the value

when Measurably expensive, pure, over inputs that are stable most of the time.

cost Memory, comparison, and a dependency list you must keep correct.

Memoize the component

when A measurably expensive render, with props that genuinely rarely change.

cost A prop comparison per render; worthless if the render was cheap (What a Component Costs to Render).

Move it off the main thread

when Expensive, CPU-bound, over a large dataset.

cost A worker, message passing and serialization — the right answer when caching only reduces how often something unacceptable happens (When a Worker Is Actually the Answer).

The memo that never hits
Unstable input
function Table({ rows }) {
  // new object literal every render
  const options = { sort: 'name', dir: 'asc' }

  // depends on `options`, which is never the same reference
  const sorted = useMemo(
    () => sortRows(rows, options),
    [rows, options],       // always "changed"
  )
  // -> sortRows runs every render, plus the comparison.
  //    Strictly slower than not memoizing at all.
}
Stable input
const OPTIONS = { sort: 'name', dir: 'asc' }   // module scope

function Table({ rows }) {
  const sorted = useMemo(
    () => sortRows(rows, OPTIONS),
    [rows],                // only changes when the data does
  )
  // -> hits whenever rows are unchanged.
  //    Now worth having — if sortRows was expensive.
}

The first version pays for the cache and never uses it, which is the single most common memoization defect and is invisible without a profile. The fix is upstream — stabilise the input — not a different memo. And the last comment matters: even the corrected version only earns its place if sortRows was actually expensive.

The correctness half

It is worth separating the two failure modes, because they are treated as one and are not. A memo that is too coarse is a performance disappointment. A memo that is missing a dependency is a wrong answer displayed with confidence, and it will be reported as a data bug rather than a caching one.

That is the strongest argument for restraint: every memo is a place where the UI can disagree with the state, and a codebase with hundreds of them has hundreds of such places.

  • The two questions are separate and both required: is this expensive (profile it) and does the cache hit (check the inputs).
  • A memo is a place where the UI can go stale. Fewer of them means fewer such places (State Synchronization).
  • Compiler-inserted memoization changes who writes the code, not whether the memory and comparison costs exist.
Memoization failures and what they look like
TriggerSymptomCauseResponse
Dependency omittedUI shows a value that is no longer trueThe memo did not recompute because the changed input was not declaredComplete the dependency list; enforce it with a linter — this is a correctness bug.
Input recreated each renderMemo never hits; profile shows the work still runningReference equality fails on a fresh object or array literalHoist or stabilise the input rather than adding another memo.
Cheap computation memoizedMarginally slower, more codeComparison and storage exceed the work avoidedRemove it; keep memoization for measured costs.
Unbounded key spaceHeap grows for the life of the sessionA memo cache keyed by user input with no evictionBound it — LRU with a ceiling (Memory Leaks).
Deep equality on a large objectComparison dominates the profileThe equality check is more expensive than recomputingCompare an identity or a version, not the contents.
Side effect inside a memoBehaviour changes with cache hitsThe effect now runs only on missMemoize pure computation only; effects belong elsewhere.

How to build it

Most important first.

  • Measure first. Memoization is a fix for a measured cost. Applied speculatively it is complexity with an unknown payoff (Measure Before Optimising).
  • Prefer removing the work over caching it. Deriving less, computing outside a render, or restructuring so the value is not recomputed at all beats caching (Derived State).
  • Stabilise the inputs before reaching for a memo. Most memos that never hit are downstream of an object literal created on every render.
  • Keep memoized functions pure. Memoizing anything with a side effect changes how often that effect runs, which is a behavioural change disguised as an optimisation.
  • Bound any cache with an unbounded key space — an LRU with a ceiling, not a growing map (The Client Cache Model).
  • Get the dependencies right, and let a linter enforce them. This is the correctness half and it is where the real bugs are.
  • For an expensive computation over a large dataset, consider whether it belongs on the main thread at all before optimising how often it runs (When a Worker Is Actually the Answer).

Keyboard, focus, semantics, announcement

A required field on every lesson in this domain, not a section added when there is room.

  • A stale memoized value can render stale text, and if that text is inside a live region the announcement is wrong rather than merely late — a correctness bug that reaches screen-reader users as misinformation (Live Regions and Announcement).
  • The interactions memoization is usually deployed to rescue — typing, filtering, dragging — are the ones where delay is most disabling for users relying on switch access, voice control or eye tracking, since each of their inputs is more effortful (Interaction Responsiveness).
  • Retained memory shortens the life of a long session, and a tab discarded mid-task costs a keyboard or screen-reader user more to recover from than a sighted mouse user (Memory Leaks).

What can go wrong

Failure modes
  • A memo that never hits, because an input is recreated every render — pure overhead, and invisible without profiling.
  • A missing dependency producing a stale value, which presents as a data bug rather than a caching one.
  • An unbounded memo cache growing for the life of the session.
  • Memoizing a component whose render was cheap, so the comparison exceeds the saving.
  • A deep-equality comparison over a large object costing more than the computation it guards.
  • Memoization masking an underlying problem — an expensive computation running far more often than it should, cached instead of fixed.
Security
  • A memo keyed by user-controlled input with no bound is a denial-of-service vector against the user's own device: unique keys mean unbounded growth.
  • Cached results retain whatever was computed, including personal data, for longer than intended — worth considering for anything sensitive (Storage Security and Durability).
  • A memoized authorization-shaped computation can serve a stale answer after permissions change, which is one more reason such decisions belong on the server (Authorization-Aware UI).
Misreads
  • "Memoization cannot make things slower." It routinely does, when comparison and retention exceed the work avoided.
  • "Memoize everything to be safe." It is a listed forbidden claim in this domain for a reason: it adds cost, adds bugs, and hides the real hotspot.
  • "It is a performance concern." A wrong dependency list is a correctness bug that produces stale UI, and that is the more common failure.
  • "The compiler handles it now." Compiler-driven memoization removes much of the manual work; it does not remove the memory cost or the need to know whether the work was expensive.
  • "My memo is not helping, so memoization does not work here." Usually an input is being recreated upstream — fix the input, not the memo.

Measuring it, and what changes in the field

How you would see this
  • A profile before and after. If the difference is not visible in a recording, the memo is not earning its complexity (Debugging Rendering and Jank).
  • Hit rate. A memo that rarely hits is overhead, and the usual cause is an unstable input.
  • Interaction latency in the field, which is the outcome the optimisation exists to move (Interaction Responsiveness).
  • Heap growth over a session, to catch an unbounded cache (Debugging Memory).
Slow device, slow network, large data, old tab
  • On a slow device the threshold at which memoization pays arrives sooner, so a memo that is pointless on a laptop can be worthwhile on a phone — which is an argument for profiling throttled, not for memoizing everything.
  • With a large dataset the computation may be expensive enough that the answer is a worker rather than a cache.
  • In a long session, retention matters more than in a short one, and an unbounded memo is a slow leak.
What this costs
  • Skipped work, in exchange for memory, comparison cost, and an invalidation surface that can produce wrong output.
  • Coarse memoization is cheap to compare and hits rarely; fine-grained memoization hits often and costs more to compare.
  • Removing the computation is strictly better than caching it and is usually more work up front.

Where this applies

Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.

  • GENERALThe trade of computation for memory plus an invalidation obligation is a property of caching itself, so it holds regardless of language, framework or where the cache lives.
  • FRAMEWORK-SPECIFICThe mechanics differ sharply: React memoizes explicitly with dependency arrays and is moving toward compiler-inserted memoization; Vue and Solid track dependencies automatically so computed values invalidate without a declared list; Svelte resolves much of it at compile time. The stale-dependency failure mode is largely a React-shaped problem, while the cost-versus-benefit question applies everywhere (Reactivity Models).
  • DEVICE-SPECIFICWhether a given computation is expensive enough to be worth caching depends on the CPU running it, so a memo that measures as pointless on a development machine can be worthwhile on a mid-range phone — profile throttled before concluding either way.

Where the depth lives

This domain teaches the browser-side mechanism and hands the rest off.

Domains that do not exist yet
  • Programming Languages & Runtime Internals — engines already cache and optimise aggressively, which is part of why hand-written memoization of small computations so often fails to pay.
  • Software Design — a cache is a second source of truth, and every one of them is a place where two answers can disagree.