micro-memoize

repository·main·Indexed 42 days ago

https://github.com/planttheidea/micro-memoize

A high-performance, lightweight memoization library version 5.2.0 designed to cache function results based on their arguments. It features LRU eviction via maxSize, async promise support, custom key equality (deep/shallow), and detailed statistics collection. Compatible with Node 6+ and modern browsers.

Tokens
9.7K
Snippets
30
Records
39
Agent score
53%

What's inside micro-memoize

  1. Performance Benchmarks

    main

    micro-memoize is highly optimized for performance, particularly in scenarios involving multiple parameters. In benchmarks performed with tinybench on NodeJS 24.15.0, micro-memoize consistently outperformed other libraries like lru-memoize, fast-memoize, lodash, and ramda across various parameter types (primitives, arrays, and objects).

    Key performance characteristics:

    • Multiple Parameter Support: Unlike underscore, lodash, and ramda, micro-memoize supports multiple-parameter memoization natively without requiring a custom resolver (e.g., JSON.stringify).
    • High Throughput: Consistently achieves high operations per second (ops/sec) across single and multiple argument patterns.
  2. Compose memoized functions with different options

    main

    Starting in version 4.0.0, you can compose memoized functions to create multiple versions of a function with different configuration options.

    When composing, the original function is used as the base, and the composition layers only apply the new options to the configuration. For example, if you compose a memoized function memoized with new options, the resulting function still executes the original underlying function fn, but uses the merged options from all layers.

    const toObject = (one: string, two: string) => ({ one, two });
    const memoized = memoize(toObject);
    
    // Create versions with different option sets
    const withUpToFive = memoize(memoized, { maxSize: 5 });
    const withAsync = memoize(withUpToFive, { async: true });
    const withCustomEquals = memoize(withAsync, { isEqual: deepEqual });
  3. Collect and retrieve memoization statistics

    main

    You can monitor the effectiveness of your memoization by collecting statistics. To enable this, you must provide a statsName in the options when calling memoize.

    Warning: It is recommended not to activate statistics collection in production due to a small performance impact.

    Workflow

    1. Call startCollectingStats() to begin monitoring.
    2. Define memoized functions with a statsName.
    3. Use getStats(statsName?) to retrieve data.
    4. Use stopCollectingStats() to stop.
    5. Use clearStats(statsName?) to reset data.

    Statistics Format

    getStats() returns an object containing calls, hits, name, and usage (percentage). If called without arguments, it returns a global summary including a profiles object containing stats for all named functions.

    import { getStats, memoize, startCollectingStats, stopCollectingStats, clearStats, isCollectingStats } from 'memoize';
    
    startCollectingStats();
    
    const foo = memoize((one: string, two: string) => [one, two], {
      statsName: 'foo',
    });
    
    foo('one', 'two');
    foo('one', 'two');
    
    console.log(getStats('foo'));
    /*
    {
      "calls": 2,
      "hits": 1,
      "name": "foo",
      "usage": "50.000000%"
    }
    */
    
    stopCollectingStats();
  4. Listen to cache events

    main

    You can attach listeners to a memoized function's cache to react to changes. The CacheEventType includes:

    • 'add': Fired when a new entry is added.
    • 'delete': Fired when an entry is removed.
    • 'hit': Fired when an existing entry is retrieved.
    • 'update': Fired when an entry is updated (e.g., updating recency or resolving an async entry).

    Each event provides a CacheEvent object containing:

    • cache: The Cache instance.
    • key: The Key (array of arguments) of the affected node.
    • value: The cached value.
    • type: The event type.
    • reason: (Optional) A string explaining why the operation occurred.
  5. Manage cache performance metrics with stats functions

    main

    You can monitor the performance of your memoized functions (cache hits vs. total calls) using the built-in statistics API.

    To begin collecting data, you must call startCollectingStats(). Once active, you can retrieve metrics for specific memoized methods or for the entire application.

    Workflow:

    1. Call startCollectingStats() to enable tracking.
    2. Use getStats() to retrieve data.
    3. Use stopCollectingStats() to disable tracking and stop listeners.
    import { startCollectingStats, getStats, stopCollectingStats } from 'micro-memoize/stats';
    
    // 1. Enable collection
    startCollectingStats();
    
    // ... run your memoized functions ...
    
    // 2. Retrieve global stats
    const stats = getStats();
    console.log(stats);
    
    // 3. Stop collection
    stopCollectingStats();
  6. Basic usage of memoize

    main

    To memoize a function, pass the target function to memoize(). Subsequent calls with the same arguments will return the cached result instead of re-executing the original function.

    const toObject = (one: string, two: string) => ({ one, two });
    
    const memoized = memoize(toObject);
    
    console.log(memoized('one', 'two'));
    console.log(memoized('one', 'two')); // pulled from cache
  7. Limit cache size with maxSize

    main

    The maxSize option (defaults to 1) limits the number of values stored in the cache using a Least Recently Used (LRU) eviction policy. When the limit is reached, the least recently used entry is removed to make room for new entries.

    const manyPossibleArgs = (one: string, two: string) => [one, two];
    const memoized = memoize(manyPossibleArgs, { maxSize: 3 });
    
    memoized.cache.on('delete', (event) => console.log('Deleted from cache: ', event.key));
    
    memoized('one', 'two');
    memoized('two', 'three');
    memoized('three', 'four');
    
    // Adding a 4th unique entry triggers LRU eviction
    memoized('four', 'five'); // ['four', 'five'], Deleted from cache: ['one', 'two']
  8. Transform cache keys with transformKey

    main

    The transformKey option allows you to map the arguments passed to the function into a different set of arguments to be used for caching. This is useful if you want to ignore certain arguments (like functions or large objects) when determining a cache hit.

    Processing Order: If combined with maxArgs or serialize, the order is:

    1. transformKey (if applicable)
    2. maxArgs (if applicable)
    3. serialize (if applicable)

    If your transformed keys require custom equality logic, you can combine transformKey with isKeyEqual.

    // Ignore a function argument by only using the first argument for the key
    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
  9. Configure async memoization

    main

    When the memoized function returns a Promise, set the async: true option. This enables specific cache event behaviors:

    • On Resolution: Fires the hit and update cache events.
    • On Rejection: Triggers automatic removal of the entry from the cache and fires the delete cache event.

    Note: If you want to prevent rejections from auto-removing entries from the cache, set async: false (or omit it), but note that you will also lose the cache listeners that fire on successful resolution.

    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, { async: 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);
  10. Configure cache expiration with expires

    main

    The expires option controls how long a computed value remains in the cache. It accepts several formats:

    1. Number: Milliseconds until expiration.
    2. Function: A predicate (key, value) => boolean to determine expiration dynamically.
    3. Configuration Object: For advanced conditional expiration.

    Tip: When using expires with async functions (like AJAX calls), you should also apply the update: true option so the expiration countdown begins upon the promise's resolution rather than its invocation.

    // Using a fixed millisecond value
    const MAX_AGE = 1000 * 60 * 5;
    const expiringMemoized = memoize(fn, { expires: MAX_AGE });
    
    // Using a dynamic function
    const expiringMemoized = memoize(fn, {
      expires: (key, value) => shouldExpire(key, value),
    });
    
    // Using a configuration object
    const conditionalExpiringMemoized = memoize(fn, {
      expires: {
        after: MAX_AGE,
        shouldPersist: (item) => !item.expires,
        shouldRemove: (item) => item.updatedAt < new Date('2025-01-01').valueOf(),
        update: true,
      },
    });
  11. Inspect memoized function properties

    main

    A memoized function provides several properties for introspection:

    • memoized.fn: The original function passed to memoize.
    • memoized.isMemoized: A boolean hard-coded to true when the function is memoized.
    • memoized.options: The options object passed during creation.