• Stars
    star
    238
  • Rank 169,306 (Top 4 %)
  • Language
    TypeScript
  • License
    MIT License
  • Created almost 7 years ago
  • Updated 4 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

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

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, with the exception of the default being different.

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

NOTE: The default for micro-memoize differs from the default implementation of moize. moize will store an infinite number of results unless restricted, whereas micro-memoize will only store the most recent result. In this way, the default implementation of micro-memoize operates more like moize.simple.

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)

More Repositories

1

fast-copy

A blazing fast deep object copier
JavaScript
1,133
star
2

moize

The consistently-fast, complete memoization solution for JS
TypeScript
892
star
3

fast-equals

A blazing fast equality comparison, either shallow or deep
TypeScript
471
star
4

unchanged

A tiny, fast, unopinionated handler for updating JS objects and arrays immutably
TypeScript
240
star
5

crio

Immutable objects and arrays in a natural way
JavaScript
211
star
6

hash-it

Hash any object type based on its values
TypeScript
206
star
7

remeasure

Get position and size attributes for any React Component
JavaScript
140
star
8

inline-loops.macro

Iteration helpers that inline to native loops for performance
JavaScript
100
star
9

selectorator

Simple generator of reselect selectors
TypeScript
96
star
10

react-style-tag

Write styles declaratively in React
TypeScript
68
star
11

react-pure-lifecycle

JavaScript
65
star
12

react-windowed-list

JavaScript
61
star
13

jile

Modular CSS in pure JavaScript
JavaScript
59
star
14

react-vidz-player

HTML5 videos in a React way
JavaScript
53
star
15

fast-stringify

A blazing fast stringifier that safely handles circular objects
TypeScript
51
star
16

react-billboardjs

React component for the billboard.js charting library
JavaScript
45
star
17

curriable

Curry any function with placeholder support
TypeScript
38
star
18

vidz

A zero-dependency, framework-agnostic video implementation
JavaScript
37
star
19

switchem

An extensible, functional switch with a chainable API
JavaScript
35
star
20

waddup

A ridiculously tiny pubsub manager with no dependencies
JavaScript
27
star
21

benchee

Simple benchmarks in both node and browser
TypeScript
27
star
22

react-local-redux

Manage component-specific state as you would global state via redux
JavaScript
23
star
23

arco

JavaScript
22
star
24

flexor

JavaScript
21
star
25

qonductor

Manage your data processing with sanity
JavaScript
19
star
26

react-parm

Handle react class instances with more functional purity
JavaScript
18
star
27

react-rendered-size

Get the rendered size of a React element without needing to render it
JavaScript
17
star
28

kari

JavaScript
14
star
29

pathington

JavaScript
13
star
30

get-object-class

A more explicit improvement on typeof
JavaScript
12
star
31

redux-browser-storage

Use redux to manage localStorage and sessionStorage data
JavaScript
11
star
32

remodeled

An abstraction for the React API with functional purity
JavaScript
11
star
33

convertify

Easily convert from one object class to the next
JavaScript
10
star
34

nage

Efficient, tiny object pool
TypeScript
9
star
35

tcf

A functional try / catch / finally with async support
JavaScript
8
star
36

bolster-css

JavaScript
7
star
37

printscout

Handle print events with ease
JavaScript
7
star
38

pure-object

JavaScript
7
star
39

react-jile

JavaScript
7
star
40

highcharts-config

Declarative Highcharts configuration generator with immutable, chainable API
JavaScript
6
star
41

react-idle-manager

JavaScript
6
star
42

react-redux-partitioner

Distribute state management for more performant reactivity
TypeScript
5
star
43

repoll

JavaScript
5
star
44

isit.js

Micro check library
JavaScript
4
star
45

identitate

Custom identity functions for composability
JavaScript
4
star
46

retip

A simple react tooltip
JavaScript
3
star
47

redux-slices

Manage slices of redux store in a concise, clear way
HTML
2
star
48

utilities

A collection of utilities used across projects
JavaScript
2
star
49

doozy

Transducer library for arrays, objects, sets, and maps
JavaScript
2
star
50

what-am-i

Simple validation library
TypeScript
2
star
51

promise-polyfill

Promise polyfill with custom opt-in error handling for debug
TypeScript
1
star
52

bolster

Library to augment jQuery with additional functionality
JavaScript
1
star
53

memzee

Function memoization based on only the most recent arguments
TypeScript
1
star
54

isifier

Make your own tiny, targeted validation library
JavaScript
1
star
55

diviso

Simple, flexible state management
1
star
56

singulum

State management with sanity
JavaScript
1
star
57

planttheidea.github.io

Github IO site for planttheidea
JavaScript
1
star