February 12, 2024

Optimize like a pro: Memoization in Javascript

Memoization is a caching technique that consists in saving the result of an expensive function and returning it if the same inputs are provided again. In short, it’s like having a “no smoking” sign inside a restaurant instead of going to each customer and asking them not to light their cigarettes.

Being a design pattern and not a syntax, it doesn’t exist only in Javascript, and it doesn’t need any particular library or technology to be implemented.

Memoization made easy

The basic concept of memoization is that it’s possible to save the result of a repeated computation for future reference in order to retrieve it in cache instead of computing it many times and so make your program execution faster.

It seems more complex than it is, let’s break it down with a practical example.

Suppose we have a function that multiplies any number by 1000:

const multiplyBy1000 = (num) => {
  return num * 1000
}

multiplyBy1000(10) // the result is 10000
multiplyBy1000(10) // the result is still 10000
multiplyBy1000(10) // the result is 10000 again
multiplyBy1000(10) // Guess what the result is? 10000
multiplyBy1000(10) // the result is 39. Just jocking it's 10000
multiplyBy1000(10) // Wait... I did the same calculation 6 times!

As you can see we have done the same calculation 6 times! it’s not a big deal if the calculation is simple like in this example, but if it’s expensive we need to optimize.

Modern Times, Charlie Chaplin 1936

const multiplyBy1000 = () => {
  let _memo = {}
  return function(n) {
    if (n in _memo) {
      // result is in memo. Return from there
      return _memo[n]; 
    }
    // result is not in memo. Calculate and save the result in memo
    let result = n * 1000;
    _memo[n] = result;
    return result;
  }
}
const memoizeMultiplyBy1000 = multiplyBy1000();

memoizeMultiplyBy1000(10) // the result is calculated
memoizeMultiplyBy1000(10) // now the result is taken from memo

Have a look. We created a multiplyBy1000() function that contains a _memo variable where we want to store our cached result. Keep in mind that the _memo scope is the function itself, so it can’t be accessed or modified from the outside, but it can from a nested function. Every time we call it, the function will check if the value we are calling it for is already saved in the memo object. In that case the result will be just returned, otherwise the calculation will be performed.

Let’s make it reusable

The example we just gave is not very common in daily work. Usually we use a function that accepts a function as an input and returns the result of that function from the cache if it exists, otherwise it executes the function. Major frameworks and libraries under the hood work that way.

Again, this is easier said than done:

const memoizer = (fn) => {
  let _memo = {};
  return (...params) => {
    if (params.toString() in _memo) {
      // result is in memo. Return from there
      return _memo[params.toString()]
    }
    // result is not in memo. Execute the function and save the result in the memo
    const result = fn(...params)
    _memo[params.toString()] = result;
    result;
  }
}
const memoizeMultiplyBy1000 = memoizer(function(n) {
  return n * 1000;
});
memoizeMultiplyBy1000(10) // the result is calculated
memoizeMultiplyBy1000(10) // now the result is taken from cache

So the function memoizeMultiplyBy1000() is executed only if the result is not in memo.

Key concepts

  • Pure functions: A function is called pure when returns the same result every single time given the same set of inputs. This is essential in memoization, because we shouldn’t risk caching variable results based on external factors (such as API calls for example)
  • Closures: A closure function is a function that is able to access variables form its outer scope. In the memoization we use it to return or update the value of the memo variable in from the returned function.
  • Decorators: Decorators are a way to wrap functions in other functions in order to enhance them with new behaviors. typically they take a function as input and provide another function as output. In this way we have the possibility to skip the execution of a function if its result is already saved in memory.

Memoization in the Javascript world

As I mentioned, Memoization is not linked to a particular technology but is a common pattern of software development. Let’s take a quick look at how it is implemented in some frameworks and libraries of the Javascript world.

Irene Iaccio

Freelance web developer