Top Rated Plus on Upwork with a 100% Job Success ScoreView on Upwork
retzdev logo
logo
tech

Typescript Memoization

by Jarrett Retz

January 24th, 2024 typescript web development performance optimization programming javascript react

Introduction

A while back I saw a complex memoization function in Javascript that I didn't understand. The function used Map and WeakMap: two objects I never used or even knew how to use.

Therefore, I set out to get a little closer to understanding the differences between the two and how they relate to writing memoization functions.

Terms

First, let's begin with what memoization is:

In computingmemoization or memoization is an optimization technique used primarily to speed up computer programs by storing the results of expensive function calls to pure functions and returning the cached result when the same inputs occur again.

Wikipedia

Here is an example of a higher-order function that memoizes a different function:

// https://javascript.plainenglish.io/efficient-js-memoization-using-map-86f1f4735cc

function memo(fn) {
  const cache = new Map();
  
  return function(...args) {
    const key = args.map(arg -> `${arg}_${typeof arg}`).join('|');
    
    if (cache.has(key)) {
      return cache.get(key);
    }
    
    const result = fn(...args);
    
    cache.set(key, result);
    return result;
  }
}

I wanted to find an example that used Map():

The Map object holds key-value pairs and remembers the original insertion order of the keys. Any value (both objects and primitive values) may be used as either a key or a value.

MDN Web Docs

There are a lot of differences, actually, between a Map and a normal Object, to list a few (full list):

  • "A Map is an iterable, so it can be directly iterated."
  • "A map, Performs better in scenarios involving frequent additions and removals of key-value pairs."
  • "A Map's keys can be any value (including functions, objects, or any primitive)".

Finding a Function

I wanted to find an expensive function to demonstrate memoization, which took me longer than I had hoped. Anyway, I knew combination and permutation functions are expensive, and found one on SO:

const testFunc = (inputArr: string[]) => {
  let result: string[] = [];

  const permute = (arr: string[], m: string[]) => {
    if (arr.length === 0) {
      // @ts-ignore
      result.push(m);
    } else {
      for (let i = 0; i < arr.length; i++) {
        let curr = arr.slice();
        let next = curr.splice(i, 1);
        permute(curr.slice(), m.concat(next));
      }
    }
  };

  permute(inputArr, []);

  return result;
};

Performance

Memoization is a tradeoff between memory and performance. Consequently, it would be helpful if I could show the performance benefits. To do so, I used the following function:

function perf<T>(f: () => T): number {
  const start = performance.now();

  f();

  const stop = performance.now();

  return stop - start;
}

Memoizing a function in Typescript

Below is the memoization code, which was adapted from the original for Typescript:

function memo<T extends unknown[], A>(fn: (...args: T) => A) {
  const cache = new Map();

  return function (...args: T) {
    const key = args.map((arg) => `${arg}_${typeof arg}`).join("|");

    if (cache.has(key)) {
      return cache.get(key);
    }
    const result = fn(...args);

    cache.set(key, result);
    return result;
  };
}

Next, I passed the permutation function into the `memo` function.

const memoized = memo(testFunc);

I set up a CodeSandbox to execute the code in the browser. Running the permutation with an argument of:
const arr1 = ["luuxe", "hoebg", "qewmj", "zfpyb", "szvwh", "ndciu", "fwwdx", "pklwx", "ubrao", "pnpnn",];

The function execution took ~3 seconds the first time. However, as you might expect, running it a second time took close to ~0 milliseconds. There you have it. We memoized an expensive function.

const memoized = memo(testFunc);

// Memoize function runs
const firstRun = perf(() => memoized(arr1));
const secondRun = perf(() => memoized(arr1));

Memory

The resulting cache stores an array with 3628800 nested arrays. Assuming:

  • The average length of each array is five (there are ten items in the original array)
  • Each item in the arrays is a five-character string
  • Each character requires four bytes

We can roughly estimate the size of the one-item cache at 362 MB. We can now look at the trade-off that was made using numbers.

362 MB for ~ 3 seconds.

Although both of these numbers are estimates, it's nice to quantify the potential savings for comparison.

What about WeakMap?

Yes, back to WeakMap. Reading on MDN, a WeakMap is:

... a collection of key/value pairs whose keys must be objects or non-registered symbols, with values of any arbitrary JavaScript type, and which does not create strong references to its keys. That is, an object's presence as a key in a WeakMap does not prevent the object from being garbage collected.

MDN Web Docs


I want to put an emphasis on the last part about garbage collection:

... garbage collection is the process of removing any objects that are not being used by any other objects. Often abbreviated "GC," garbage collection is a fundamental component of the memory management system used by JavaScript.

MDN Web Docs

Bringing it full circle, we can further improve memory performance by storing objects or non-registered symbols in a WeakMap as opposed to a Map in our memoization function. This could be helpful if we have functions in our cache. Contextually, our cache may store functions if we are passing functions as keys to our cache.

Perhaps in a different article, I can demonstrate this with code. For now, it'll just have to be something we're aware of.

Jarrett Retz

Jarrett Retz is a freelance web application developer and blogger based out of Spokane, WA.

jarrett@retz.dev

Subscribe to get instant updates

Contact

jarrett@retz.dev

Legal

Any code contained in the articles on this site is released under the MIT license. Copyright 2024. Jarrett Retz Tech Services L.L.C. All Rights Reserved.