What you'll be able to do

By the end of this page you should be able to explain memoization, write a tiny memoize for functions with primitive arguments, and know when caching is the wrong move.

Previous post: throttle from zero.

Who this is for

  • People recomputing the same expensive pure calculation in a loop
  • Beginners who heard "memoize" next to React and want the JS idea first
  • Anyone optimizing before measuring — this post also says when not to

You can skip this if you already use memoization APIs with clear cache keys. This closes Phase 10 with the core idea.

The idea: remember answers

If a function always returns the same output for the same inputs and has no side effects, you can store results in a map and reuse them.

function memoize(fn) {
  const cache = new Map();
  return function (arg) {
    if (cache.has(arg)) return cache.get(arg);
    const result = fn(arg);
    cache.set(arg, result);
    return result;
  };
}

Scenario A — pure formatter. heavyFormat(id) called often with the same ids.

Scenario B — function that reads Date.now() or DOM. Caching lies — inputs look the same, world changed.

The rule: memoize pure functions; do not memoize "whatever the UI feels like now."

A concrete speed picture

function slowSquare(n) {
  // pretend expensive work
  let x = 0;
  for (let i = 0; i < 1e7; i++) x = n * n;
  return n * n;
}
const fastSquare = memoize(slowSquare);
fastSquare(9); // computes
fastSquare(9); // cache hit

Scenario A — first call pays the cost.

Scenario B — second call returns instantly from the Map.

Multiple arguments (keep it honest)

Objects as keys are awkward because {} !== {}. Start with strings/numbers, or build an explicit string key like a + "|" + b when you control the shape.

Scenario A — add(2, 3). Key "2|3".

Scenario B — memoizing with object options without a stable key — silent wrong cache hits.

A tiny practice file

<!DOCTYPE html>
<html>
  <body>
    <button id="go">Call twice</button>
    <pre id="out"></pre>
    <script>
      function memoize(fn) {
        const cache = new Map();
        return function (arg) {
          if (cache.has(arg)) return "hit:" + cache.get(arg);
          const result = fn(arg);
          cache.set(arg, result);
          return "miss:" + result;
        };
      }
      const sq = memoize(function (n) {
        return n * n;
      });
      document.getElementById("go").addEventListener("click", function () {
        const out = document.getElementById("out");
        out.textContent = sq(12) + "\n" + sq(12);
      });
    </script>
  </body>
</html>

Mistakes I see a lot

1. Caching API responses forever with no invalidation — that is a different design (with expiry), not blind memoize.

2. Memoizing impure functions that depend on globals.

3. Unbounded caches in long-lived pages — Maps grow; real apps sometimes need limits.

4. Premature memoization of tiny arithmetic — measure first; clarity beats micro-caches.

What to try before the next post

  1. Run the miss/hit demo.
  2. Break it on purpose: memoize a function that uses Math.random() and watch "wrong" repeats.
  3. Write when you would not memoize.

This closes Phase 10 — useful deeper JS. Next we begin Phase 11 — engineering habits: debugging with console and breakpoints.

Try this next outside the series

Deeper JavaScript pays off when you connect it to routes, imports, and performance-sensitive app code.