Why memoization matters

When a pure function does heavy lifting — think recursive algorithms, large data transformations, or repeated API payload processing — calling it with the same arguments over and over wastes CPU cycles. In a recent project I saw a dashboard that recomputed a 10‑million‑iteration loop on every keystroke. Adding a one‑line memoizer dropped the frame time from 250 ms to under 5 ms. The trick is not a heavyweight library; a 15‑line utility gives you 90 % of the benefit with zero dependencies.

A minimal memoize implementation

// memoize.js
export function memoize(fn, resolver) {
  const cache = new Map();
  return function (...args) {
    const key = resolver ? resolver(...args) : args.join('|');
    if (cache.has(key)) return cache.get(key);
    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

The resolver argument lets you customise the cache key. By default we join the arguments with a pipe character, which works for primitives. For objects or arrays you can supply a serializer like JSON.stringify or a custom hash function. The returned function preserves this so it can decorate methods as well.

Handling cache invalidation

Long‑lived caches can grow unbounded. Two practical patterns keep memory in check:

  • Time‑to‑live: wrap the Map with a WeakMap of timestamps and evict entries older than a threshold on each call.
  • Size limit: after inserting, if cache.size > MAX delete the oldest key (cache.delete(cache.keys().next().value)).

Both approaches add only a few lines and avoid a full‑blown LRU library when you just need a safety net.

Using it in a React component

import { memoize } from './memoize';

const expensiveCalculation = (a, b) => {
  // simulate heavy work
  let sum = 0;
  for (let i = 0; i < 1e7; i++) sum += a * b;
  return sum;
};

const memoizedCalc = memoize(expensiveCalculation, (a, b) => `${a}:${b}`);

function Dashboard({ userId }) {
  const data = useMemo(() => memoizedCalc(userId, 42), [userId]);
  return 
Result: {data}
; }

Notice the useMemo hook only re‑runs when userId changes, but the underlying memoizedCalc still protects us if the same userId appears across different components or renders. The resolver builds a stable key from the two numbers, so (5, 42) and (5, 42) hit the cache instantly.

Pitfalls and tips

  • Only memoize pure functions. Side‑effects (network calls, DOM mutations, random values) break the contract and produce stale results.
  • Watch the key strategy. args.join('|') collides for (1, '2|3') vs (1, '2', '3'). A resolver eliminates that risk.
  • Don’t memoize everything. The overhead of a Map lookup outweighs the savings for trivial functions.
Remember: memoization only helps pure functions. Side‑effects break the contract.

Next time you spot a hot function called with identical inputs, drop in this helper. It’s a five‑minute win that scales from a tiny utility script to a full‑blown UI library without any extra bundle size.