Important: This documentation covers Yarn 1 (Classic).
For Yarn 2+ docs and migration guide, see yarnpkg.com.

Package detail

micro-memoize

planttheidea1.7mMIT4.1.3TypeScript support: included

A tiny, crazy fast memoization library for the 95% use-case

memoize, memoized, memoizer, memoization, memo, cache, cached, storage, memory, optimize, performance, lru, expire

readme

micro-memoize

A tiny, crazy fast memoization library for the 95% use-case

Table of contents

Summary

As the author of moize, I created a consistently fast memoization library, but moize has a lot of features to satisfy a large number of edge cases. micro-memoize is a simpler approach, focusing on the core feature set with a much smaller footprint (~1.44kB minified+gzipped). Stripping out these edge cases also allows micro-memoize to be faster across the board than moize.

Importing

ESM in browsers:

import memoize from 'micro-memoize';

ESM in NodeJS:

import memoize from 'micro-memoize/mjs';

CommonJS:

const memoize = require('micro-memoize');

Usage

const assembleToObject = (one: string, two: string) => ({ one, two });

const memoized = memoize(assembleToObject);

console.log(memoized('one', 'two')); // {one: 'one', two: 'two'}
console.log(memoized('one', 'two')); // pulled from cache, {one: 'one', two: 'two'}

Types

If you need them, all types are available under the MicroMemoize namespace.

import { MicroMemoize } from 'micro-memoize';

Composition

Starting in 4.0.0, you can compose memoized functions if you want to have multiple types of memoized versions based on different options.

const simple = memoized(fn); // { maxSize: 1 }
const upToFive = memoized(simple, { maxSize: 5 }); // { maxSize: 5 }
const withCustomEquals = memoized(upToFive, { isEqual: deepEqual }); // { maxSize: 5, isEqual: deepEqual }

NOTE: The original function is the function used in the composition, the composition only applies to the options. In the example above, upToFive does not call simple, it calls fn.

Options

isEqual

function(object1: any, object2: any): boolean, defaults to isSameValueZero

Custom method to compare equality of keys, determining whether to pull from cache or not, by comparing each argument in order.

Common use-cases:

  • Deep equality comparison
  • Limiting the arguments compared
import { deepEqual } from 'fast-equals';

type ContrivedObject = {
  deep: string;
};

const deepObject = (object: {
  foo: ContrivedObject;
  bar: ContrivedObject;
}) => ({
  foo: object.foo,
  bar: object.bar,
});

const memoizedDeepObject = memoize(deepObject, { isEqual: deepEqual });

console.log(
  memoizedDeepObject({
    foo: {
      deep: 'foo',
    },
    bar: {
      deep: 'bar',
    },
    baz: {
      deep: 'baz',
    },
  }),
); // {foo: {deep: 'foo'}, bar: {deep: 'bar'}}

console.log(
  memoizedDeepObject({
    foo: {
      deep: 'foo',
    },
    bar: {
      deep: 'bar',
    },
    baz: {
      deep: 'baz',
    },
  }),
); // pulled from cache

NOTE: The default method tests for SameValueZero equality, which is summarized as strictly equal while also considering NaN equal to NaN.

isMatchingKey

function(object1: any[], object2: any[]): boolean

Custom method to compare equality of keys, determining whether to pull from cache or not, by comparing the entire key.

Common use-cases:

  • Comparing the shape of the key
  • Matching on values regardless of order
  • Serialization of arguments
import { deepEqual } from 'fast-equals';

type ContrivedObject = { foo: string; bar: number };

const deepObject = (object: ContrivedObject) => ({
  foo: object.foo,
  bar: object.bar,
});

const memoizedShape = memoize(deepObject, {
  // receives the full key in cache and the full key of the most recent call
  isMatchingKey(key1, key2) {
    const object1 = key1[0];
    const object2 = key2[0];

    return (
      object1.hasOwnProperty('foo') &&
      object2.hasOwnProperty('foo') &&
      object1.bar === object2.bar
    );
  },
});

console.log(
  memoizedShape({
    foo: 'foo',
    bar: 123,
    baz: 'baz',
  }),
); // {foo: {deep: 'foo'}, bar: {deep: 'bar'}}

console.log(
  memoizedShape({
    foo: 'not foo',
    bar: 123,
    baz: 'baz',
  }),
); // pulled from cache

isPromise

boolean, defaults to false

Identifies the value returned from the method as a Promise, which will result in one of two possible scenarios:

  • If the promise is resolved, it will fire the onCacheHit and onCacheChange options
  • If the promise is rejected, it will trigger auto-removal from cache
const fn = async (one: string, two: string) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(new Error(JSON.stringify({ one, two })));
    }, 500);
  });
};

const memoized = memoize(fn, { isPromise: true });

memoized('one', 'two');

console.log(memoized.cache.snapshot.keys); // [['one', 'two']]
console.log(memoized.cache.snapshot.values); // [Promise]

setTimeout(() => {
  console.log(memoized.cache.snapshot.keys); // []
  console.log(memoized.cache.snapshot.values); // []
}, 1000);

NOTE: If you don't want rejections to auto-remove the entry from cache, set isPromise to false (or simply do not set it), but be aware this will also remove the cache listeners that fire on successful resolution.

maxSize

number, defaults to 1

The number of values to store in cache, based on a Least Recently Used basis. This operates the same as maxSize on moize.

const manyPossibleArgs = (one: string, two: string) => [one, two];

const memoized = memoize(manyPossibleArgs, { maxSize: 3 });

console.log(memoized('one', 'two')); // ['one', 'two']
console.log(memoized('two', 'three')); // ['two', 'three']
console.log(memoized('three', 'four')); // ['three', 'four']

console.log(memoized('one', 'two')); // pulled from cache
console.log(memoized('two', 'three')); // pulled from cache
console.log(memoized('three', 'four')); // pulled from cache

console.log(memoized('four', 'five')); // ['four', 'five'], drops ['one', 'two'] from cache

onCacheAdd

function(cache: Cache, options: Options): void

Callback method that executes whenever the cache is added to. This is mainly to allow for higher-order caching managers that use micro-memoize to perform superset functionality on the cache object.

const fn = (one: string, two: string) => [one, two];

const memoized = memoize(fn, {
  maxSize: 2,
  onCacheAdd(cache, options) {
    console.log('cache has been added to: ', cache);
    console.log('memoized method has the following options applied: ', options);
  },
});

memoized('foo', 'bar'); // cache has been added to
memoized('foo', 'bar');
memoized('foo', 'bar');

memoized('bar', 'foo'); // cache has been added to
memoized('bar', 'foo');
memoized('bar', 'foo');

memoized('foo', 'bar');
memoized('foo', 'bar');
memoized('foo', 'bar');

NOTE: This method is not executed when the cache is manually manipulated, only when changed via calling the memoized method.

onCacheChange

function(cache: Cache, options: Options): void

Callback method that executes whenever the cache is added to or the order is updated. This is mainly to allow for higher-order caching managers that use micro-memoize to perform superset functionality on the cache object.

const fn = (one: string, two: string) => [one, two];

const memoized = memoize(fn, {
  maxSize: 2,
  onCacheChange(cache, options) {
    console.log('cache has changed: ', cache);
    console.log('memoized method has the following options applied: ', options);
  },
});

memoized('foo', 'bar'); // cache has changed
memoized('foo', 'bar');
memoized('foo', 'bar');

memoized('bar', 'foo'); // cache has changed
memoized('bar', 'foo');
memoized('bar', 'foo');

memoized('foo', 'bar'); // cache has changed
memoized('foo', 'bar');
memoized('foo', 'bar');

NOTE: This method is not executed when the cache is manually manipulated, only when changed via calling the memoized method. When the execution of other cache listeners (onCacheAdd, onCacheHit) is applicable, this method will execute after those methods.

onCacheHit

function(cache: Cache, options: Options): void

Callback method that executes whenever the cache is hit, whether the order is updated or not. This is mainly to allow for higher-order caching managers that use micro-memoize to perform superset functionality on the cache object.

const fn = (one: string, two: string) => [one, two];

const memoized = memoize(fn, {
  maxSize: 2,
  onCacheHit(cache, options) {
    console.log('cache was hit: ', cache);
    console.log('memoized method has the following options applied: ', options);
  },
});

memoized('foo', 'bar');
memoized('foo', 'bar'); // cache was hit
memoized('foo', 'bar'); // cache was hit

memoized('bar', 'foo');
memoized('bar', 'foo'); // cache was hit
memoized('bar', 'foo'); // cache was hit

memoized('foo', 'bar'); // cache was hit
memoized('foo', 'bar'); // cache was hit
memoized('foo', 'bar'); // cache was hit

NOTE: This method is not executed when the cache is manually manipulated, only when changed via calling the memoized method.

transformKey

function(Array<any>): any

A method that allows you transform the key that is used for caching, if you want to use something other than the pure arguments.

const ignoreFunctionArgs = (one: string, two: () => {}) => [one, two];

const memoized = memoize(ignoreFunctionArgs, {
  transformKey: (args) => [JSON.stringify(args[0])],
});

console.log(memoized('one', () => {})); // ['one', () => {}]
console.log(memoized('one', () => {})); // pulled from cache, ['one', () => {}]

If your transformed keys require something other than SameValueZero equality, you can combine transformKey with isEqual for completely custom key creation and comparison.

const ignoreFunctionArg = (one: string, two: () => void) => [one, two];

const memoized = memoize(ignoreFunctionArg, {
  isMatchingKey: (key1, key2) => key1[0] === key2[0],
  // Cache based on the serialized first parameter
  transformKey: (args) => [JSON.stringify(args[0])],
});

console.log(memoized('one', () => {})); // ['one', () => {}]
console.log(memoized('one', () => {})); // pulled from cache, ['one', () => {}]

Additional properties

memoized.cache

Object

The cache object that is used internally. The shape of this structure:

{
  keys: any[][], // available as MicroMemoize.Key[]
  values: any[] // available as MicroMemoize.Value[]
}

The exposure of this object is to allow for manual manipulation of keys/values (injection, removal, expiration, etc).

const method = (one: string, two: string) => ({ one, two });

const memoized = memoize(method);

memoized.cache.keys.push(['one', 'two']);
memoized.cache.values.push('cached');

console.log(memoized('one', 'two')); // 'cached'

NOTE: moize offers a variety of convenience methods for this manual cache manipulation, and while micro-memoize allows all the same capabilities by exposing the cache, it does not provide any convenience methods.

memoized.cache.snapshot

Object

This is identical to the cache object referenced above, but it is a deep clone created at request, which will provide a persistent snapshot of the values at that time. This is useful when tracking the cache changes over time, as the cache object is mutated internally for performance reasons.

memoized.fn

function

The original function passed to be memoized.

memoized.isMemoized

boolean

Hard-coded to true when the function is memoized. This is useful for introspection, to identify if a method has been memoized or not.

memoized.options

Object

The options passed when creating the memoized method.

Benchmarks

All values provided are the number of operations per second (ops/sec) calculated by the Benchmark suite. Note that underscore, lodash, and ramda do not support mulitple-parameter memoization (which is where micro-memoize really shines), so they are not included in those benchmarks.

Benchmarks was performed on an i7 8-core Arch Linux laptop with 16GB of memory using NodeJS version 10.15.0. The default configuration of each library was tested with a fibonacci calculation based on the following parameters:

  • Single primitive = 35
  • Single object = {number: 35}
  • Multiple primitives = 35, true
  • Multiple objects = {number: 35}, {isComplete: true}

NOTE: Not all libraries tested support multiple parameters out of the box, but support the ability to pass a custom resolver. Because these often need to resolve to a string value, a common suggestion is to just JSON.stringify the arguments, so that is what is used when needed.

Single parameter (primitive only)

This is usually what benchmarks target for ... its the least-likely use-case, but the easiest to optimize, often at the expense of more common use-cases.

| | Operations / second | | ----------------- | ------------------- | | fast-memoize | 59,069,204 | | micro-memoize | 48,267,295 | | lru-memoize | 46,781,143 | | Addy Osmani | 32,372,414 | | lodash | 29,297,916 | | ramda | 25,054,838 | | mem | 24,848,072 | | underscore | 24,847,818 | | memoizee | 18,272,987 | | memoizerific | 7,302,835 |

Single parameter (complex object)

This is what most memoization libraries target as the primary use-case, as it removes the complexities of multiple arguments but allows for usage with one to many values.

| | Operations / second | | ----------------- | ------------------- | | micro-memoize | 40,360,621 | | lodash | 30,862,028 | | lru-memoize | 25,740,572 | | memoizee | 12,058,375 | | memoizerific | 6,854,855 | | ramda | 2,287,030 | | underscore | 2,270,574 | | Addy Osmani | 2,076,031 | | mem | 2,001,984 | | fast-memoize | 1,591,019 |

Multiple parameters (primitives only)

This is a very common use-case for function calls, but can be more difficult to optimize because you need to account for multiple possibilities ... did the number of arguments change, are there default arguments, etc.

| | Operations / second | | ----------------- | ------------------- | | micro-memoize | 33,546,353 | | lru-memoize | 20,884,669 | | memoizee | 7,831,161 | | Addy Osmani | 6,447,448 | | memoizerific | 5,587,779 | | mem | 2,620,943 | | underscore | 1,617,687 | | ramda | 1,569,167 | | lodash | 1,512,515 | | fast-memoize | 1,376,665 |

Multiple parameters (complex objects)

This is the most robust use-case, with the same complexities as multiple primitives but managing bulkier objects with additional edge scenarios (destructured with defaults, for example).

| | Operations / second | | ----------------- | ------------------- | | micro-memoize | 34,857,438 | | lru-memoize | 20,838,330 | | memoizee | 7,820,066 | | memoizerific | 5,761,357 | | mem | 1,184,550 | | ramda | 1,034,937 | | underscore | 1,021,480 | | Addy Osmani | 1,014,642 | | lodash | 1,014,060 | | fast-memoize | 949,213 |

Browser support

  • Chrome (all versions)
  • Firefox (all versions)
  • Edge (all versions)
  • Opera 15+
  • IE 9+
  • Safari 6+
  • iOS 8+
  • Android 4+

Node support

  • 4+

Development

Standard stuff, clone the repo and npm install dependencies. The npm scripts available:

  • build => run webpack to build development dist file with NODE_ENV=development
  • build:minifed => run webpack to build production dist file with NODE_ENV=production
  • dev => run webpack dev server to run example app (playground!)
  • dist => runs build and build-minified
  • lint => run ESLint against all files in the src folder
  • prepublish => runs compile-for-publish
  • prepublish:compile => run lint, test, transpile:es, transpile:lib, dist
  • test => run AVA test functions with NODE_ENV=test
  • test:coverage => run test but with nyc for coverage checker
  • test:watch => run test, but with persistent watcher
  • transpile:lib => run babel against all files in src to create files in lib
  • transpile:es => run babel against all files in src to create files in es, preserving ES2015 modules (for pkg.module)

changelog

micro-memoize CHANGELOG

4.1.3

  • #121 - Avoid reference to broken source maps (fix for #79)

4.1.2

4.1.1 (Bad version - do not use)

  • #102 - avoid publishing development-only files for less node_modules bloat

4.1.0

Enhancements

  • Types now have direct exports instead of requiring the MicroMemoize namespace. That namespace has been labeled as deprecated, and will be removed in the next major version change in favor of the direct type exports.

Bugfixes

  • #97 - src files included in publish, and referenced from *.d.ts files
  • mjs/*d.ts files renamed to mjs/*.d.mts to align with NodeJS standard
  • #101 - fixed benchmark using mem incorrectly for complex object parameters or multiple parameter calls

4.0.15

  • #99 - mjs import does not have typings surfaced

4.0.14

  • Republish of #87

4.0.13 (Bad version - do not use)

  • #87 - Default generic values for exposed types, to avoid unintentional breaking changes from #85

4.0.12

  • #84 - Fix inferred typing of memoized function
  • #85 - Follow-up on #84, further improving inferred typing via pass-throughs.

4.0.11

  • Fix #79 - Sourcemaps referencing incorrect hierarchy

4.0.10

  • Fix #76 - noUncheckedIndexedAccess support on TS 4.1+

4.0.9

  • Update npm search keywords and documentation

4.0.8

  • Improve typings to support strict mode in TypeScript

4.0.7

  • Create more targeted getKeyIndex helpers for more speed in each key situation

4.0.6

  • Use standard then interface (.then(onFulfilled, onRejected)) instead of ES spec (.then(onFulfilled).catch(onRejected)) for broader support

4.0.5

  • Fix failure when getKeyIndex is used and no keys are in the cache

4.0.4

  • Use .pop() to cap cache to maxSize when possible (slight performance improvement)

4.0.3

  • Namespace types under MicroMemoize namespace (which is how it was for 4.0.0, but it got lost)

4.0.2

  • Make Cache class consumable in types

4.0.1

  • Fix types for consumption

4.0.0

  • Update to use Cache class instead of plain object (~10% performance improvement)

Breaking changes

  • memoized.cacheSnapshot has been deprecated in favor of memoized.cache.snapshot
  • Memoizing an already-memoized function no longer returns the function passed (now composes, see Composition)

Enhancements

  • You can now compose memoized functions with their options (see Composition)

3.0.2

  • Fix types declarations to ensure signature of fn passed is retained
  • Throw an error when the first parameter passed is not a function

3.0.1

  • Fix types declaration for Options to allow custom keys / indices

3.0.0

  • Rewrite in TypeScript
  • Use rollup for builds of all packages

BREAKING CHANGES

  • CommonJS requires no longer require .default
  • Types contract is much stricter
  • Utility methods are no longer deep-linkable
    • Not technically exposed in the API, but was relied upon by other libraries)

2.1.2

  • Fix issue where isMatchingKey was not being used with promise updater
  • Remove requirement of Object.assign existing globally
  • Add common use-case static handlers for up to 3 arguments, falling back to pre-existing dynamic handlers for more (faster comparison / argument cloning)

2.1.1

  • Upgrade to babel 7
  • Add "sideEffects": false for better tree-shaking with webpack

2.1.0

2.0.4

  • Fix issue with recursive calls to memoized function created discrepancy between keys and values in cache

2.0.3

  • More TypeScript typings (thanks again @rtorr)

2.0.2

  • Fix TypeScript typings (thanks @rtorr)

2.0.1

  • Fix TypeScript typings (thanks @Crecket)

2.0.0

  • Add isMatchingKey method to provide match test on entire key vs iterative equality

BREAKING CHANGES

  • The return value from transformKey must be an Array (would previously coalesce it for you)

NEW FEATURES

  • isMatchingKey will matching on entire key vs isEqual, which does an iterative comparison of arguments in order
  • Add size property to cache

1.8.1

  • Fix getKeyIndex being passed as memoize for promises

1.8.0

  • Include the memoized function itself as the third parameter to onCacheAdd, onCacheChange, and onCacheHit firings

1.7.0

  • Fire onCacheHit and onCacheChange when promise functions successfully resolve

1.6.3

  • Replace native slice usages with cloneArray utility

1.6.2

  • Convert dist files to be built using rollup instead of webpack

1.6.1

  • Optimize slice calls for key storage (performance)

1.6.0

  • Add onCacheAdd option
  • Pass through unused properties in options for higher-order memoization library usage

1.5.0

1.4.0

  • Add options as second parameter to onCacheChanged

1.3.2

  • Make additional properties (cache, cacheSnapshot, isMemoized, options) configurable for higher-order memoization library usage

1.3.1

  • Only reorder keys when matching cache entry is not first key

1.3.0

1.2.0

  • Add isPromise option
  • Add typings for Flowtype and TypeScript

1.1.0

1.0.1

  • Delay argument-to-key generation until stored as new cache value (speed improvement of ~35%)

1.0.0

  • Initial release