Skip to main content

JavaScript Performance

JavaScript Memoization

Written by Published

Memoization caches a function's results so the same input never does the same work twice.

It trades memory for speed.

It only helps for a pure function, where the same input always gives the same output.

Example

Example

javascript

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;
  };
}

let calls = 0;
const double = memoize(function (n) {
  calls++;
  return n * 2;
});

double(5);
double(5);
double(5);

console.log(calls);

The output is 1: the real function ran only for the first call.

Caching by Argument

The first time an input is seen, the function runs and the result is stored.

Every later call with that same input returns the stored result instantly.

A Map is a natural cache, keyed by the argument.

Syntax

Syntax

javascript

const memoized = memoize(expensiveFunction);

Only worth it for a function that is genuinely expensive to call.

Seeing the Cache Work

Repeated calls with the same input never reach the real function again.

Example

Example

javascript

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;
  };
}

let calls = 0;
const square = memoize(function (n) {
  calls++;
  return n * n;
});

console.log(square(4));
console.log(square(4));
console.log(calls);

The output is 16, 16, 1.

Different Inputs Are Cached Separately

Each distinct argument gets its own cache entry.

Example

Example

javascript

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;
  };
}

let calls = 0;
const square = memoize(function (n) {
  calls++;
  return n * n;
});

square(2);
square(3);
square(2);

console.log(calls);

The output is 2: two distinct inputs, two real calls.

Speeding Up Recursion

Naive recursive Fibonacci repeats the same calculation constantly without a cache.

Example

Example

javascript

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

let fib;
fib = memoize(function (n) {
  return n <= 1 ? n : fib(n - 1) + fib(n - 2);
});

console.log(fib(20));

Without memoizing, this would repeat the same smaller calls thousands of times.

Keys Beyond a Single Number

For several arguments, build a single string key from all of them.

Example

Example

javascript

function memoize(fn) {
  const cache = new Map();
  return function (a, b) {
    const key = a + "," + b;
    if (cache.has(key)) return cache.get(key);
    const result = fn(a, b);
    cache.set(key, result);
    return result;
  };
}

let calls = 0;
const add = memoize(function (a, b) {
  calls++;
  return a + b;
});

add(2, 3);
add(2, 3);

console.log(calls);

The output is 1, even though two arguments are involved.

When Not to Memoize

A function with side effects, or one that depends on more than its arguments, cannot be cached safely.

Caching a rarely-repeated call also just wastes memory for no benefit.

Example

Example

javascript

function notPure() {
  return Math.random();
}

console.log(typeof notPure());

Memoizing this would return the same random value forever, which is wrong.

Complete Example

Complete Example

html

<!DOCTYPE html>
<html>
<head>
  <title>JavaScript Memoization</title>
</head>
<body>

  <h1>Memoization</h1>

  <p id="out"></p>

  <script>
    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;
      };
    }

    let realCalls = 0;
    const slowSquare = memoize(function (n) {
      realCalls++;
      return n * n;
    });

    slowSquare(5);
    slowSquare(5);
    slowSquare(5);

    document.getElementById("out").textContent =
      "Result: " + slowSquare(5) + ", real calls: " + realCalls;
  </script>

</body>
</html>

Try It Yourself

Run the above example in the Try It Editor.

Try a new input:

Call slowSquare(6) as well and watch realCalls go up by one.

Important Points

  • Memoization caches a function's results by its input.
  • A Map makes a natural cache.
  • Only a pure function can be memoized safely.
  • Multiple arguments need a combined key.
  • It is not worth it for a cheap or rarely-repeated call.

Conclusion

Memoization is a small pattern with a large effect on repeated expensive work.

Recursive functions like Fibonacci show the effect most dramatically.

Remember that it only ever helps for pure functions.