Home

Awesome

memoize

Memoize functions - An optimization used to speed up consecutive function calls by caching the result of calls with identical input

Memory is automatically released when an item expires or the cache is cleared.

<!-- Please keep this section in sync with https://github.com/sindresorhus/p-memoize/blob/main/readme.md -->

By default, only the memoized function's first argument is considered via strict equality comparison. If you need to cache multiple arguments or cache objects by value, have a look at alternative caching strategies below.

If you want to memoize Promise-returning functions (like async functions), you might be better served by p-memoize.

Install

npm install memoize

Usage

import memoize from 'memoize';

let index = 0;
const counter = () => ++index;
const memoized = memoize(counter);

memoized('foo');
//=> 1

// Cached as it's the same argument
memoized('foo');
//=> 1

// Not cached anymore as the argument changed
memoized('bar');
//=> 2

memoized('bar');
//=> 2

// Only the first argument is considered by default
memoized('bar', 'foo');
//=> 2
Works well with Promise-returning functions

But you might want to use p-memoize for more Promise-specific behaviors.

import memoize from 'memoize';

let index = 0;
const counter = async () => ++index;
const memoized = memoize(counter);

console.log(await memoized());
//=> 1

// The return value didn't increase as it's cached
console.log(await memoized());
//=> 1
import memoize from 'memoize';
import got from 'got';
import delay from 'delay';

const memoizedGot = memoize(got, {maxAge: 1000});

await memoizedGot('https://sindresorhus.com');

// This call is cached
await memoizedGot('https://sindresorhus.com');

await delay(2000);

// This call is not cached as the cache has expired
await memoizedGot('https://sindresorhus.com');

Caching strategy

By default, only the first argument is compared via exact equality (===) to determine whether a call is identical.

import memoize from 'memoize';

const pow = memoize((a, b) => Math.pow(a, b));

pow(2, 2); // => 4, stored in cache with the key 2 (number)
pow(2, 3); // => 4, retrieved from cache at key 2 (number), it's wrong

You will have to use the cache and cacheKey options appropriate to your function. In this specific case, the following could work:

import memoize from 'memoize';

const pow = memoize((a, b) => Math.pow(a, b), {
  cacheKey: arguments_ => arguments_.join(',')
});

pow(2, 2); // => 4, stored in cache with the key '2,2' (both arguments as one string)
pow(2, 3); // => 8, stored in cache with the key '2,3'

More advanced examples follow.

Example: Options-like argument

If your function accepts an object, it won't be memoized out of the box:

import memoize from 'memoize';

const heavyMemoizedOperation = memoize(heavyOperation);

heavyMemoizedOperation({full: true}); // Stored in cache with the object as key
heavyMemoizedOperation({full: true}); // Stored in cache with the object as key, again
// The objects appear the same, but in JavaScript, they're different objects

You might want to serialize or hash them, for example using JSON.stringify or something like serialize-javascript, which can also serialize RegExp, Date and so on.

import memoize from 'memoize';

const heavyMemoizedOperation = memoize(heavyOperation, {cacheKey: JSON.stringify});

heavyMemoizedOperation({full: true}); // Stored in cache with the key '[{"full":true}]' (string)
heavyMemoizedOperation({full: true}); // Retrieved from cache

The same solution also works if it accepts multiple serializable objects:

import memoize from 'memoize';

const heavyMemoizedOperation = memoize(heavyOperation, {cacheKey: JSON.stringify});

heavyMemoizedOperation('hello', {full: true}); // Stored in cache with the key '["hello",{"full":true}]' (string)
heavyMemoizedOperation('hello', {full: true}); // Retrieved from cache

Example: Multiple non-serializable arguments

If your function accepts multiple arguments that aren't supported by JSON.stringify (e.g. DOM elements and functions), you can instead extend the initial exact equality (===) to work on multiple arguments using many-keys-map:

import memoize from 'memoize';
import ManyKeysMap from 'many-keys-map';

const addListener = (emitter, eventName, listener) => emitter.on(eventName, listener);

const addOneListener = memoize(addListener, {
	cacheKey: arguments_ => arguments_, // Use *all* the arguments as key
	cache: new ManyKeysMap() // Correctly handles all the arguments for exact equality
});

addOneListener(header, 'click', console.log); // `addListener` is run, and it's cached with the `arguments` array as key
addOneListener(header, 'click', console.log); // `addListener` is not run again because the arguments are the same
addOneListener(mainContent, 'load', console.log); // `addListener` is run, and it's cached with the `arguments` array as key

Better yet, if your function’s arguments are compatible with WeakMap, you should use deep-weak-map instead of many-keys-map. This will help avoid memory leaks.

API

memoize(fn, options?)

fn

Type: Function

The function to be memoized.

options

Type: object

maxAge

Type: number
Default: Infinity

Milliseconds until the cache entry expires.

cacheKey

Type: Function
Default: arguments_ => arguments_[0]
Example: arguments_ => JSON.stringify(arguments_)

Determines the cache key for storing the result based on the function arguments. By default, only the first argument is considered.

A cacheKey function can return any type supported by Map (or whatever structure you use in the cache option).

Refer to the caching strategies section for more information.

cache

Type: object
Default: new Map()

Use a different cache storage. Must implement the following methods: .has(key), .get(key), .set(key, value), .delete(key), and optionally .clear(). You could for example use a WeakMap instead or quick-lru for a LRU cache.

Refer to the caching strategies section for more information.

memoizeDecorator(options)

Returns a decorator to memoize class methods or static class methods.

Notes:

options

Type: object

Same as options for memoize().

import {memoizeDecorator} from 'memoize';

class Example {
	index = 0

	@memoizeDecorator()
	counter() {
		return ++this.index;
	}
}

class ExampleWithOptions {
	index = 0

	@memoizeDecorator({maxAge: 1000})
	counter() {
		return ++this.index;
	}
}

memoizeClear(fn)

Clear all cached data of a memoized function.

fn

Type: Function

The memoized function.

Tips

Cache statistics

If you want to know how many times your cache had a hit or a miss, you can make use of stats-map as a replacement for the default cache.

Example

import memoize from 'memoize';
import StatsMap from 'stats-map';
import got from 'got';

const cache = new StatsMap();
const memoizedGot = memoize(got, {cache});

await memoizedGot('https://sindresorhus.com');
await memoizedGot('https://sindresorhus.com');
await memoizedGot('https://sindresorhus.com');

console.log(cache.stats);
//=> {hits: 2, misses: 1}

Related