mnemonist review
Mnemonist 0.40.4 puts dozens of mutable JavaScript data structures in one package, ranging from ordinary heaps, queues, tries, LRU maps, and multisets to BK trees, VP trees, Bloom filters, suffix arrays, interval indexes, and fixed-capacity buffers. It deliberately omits graphs. Each class exposes its own capacity, iteration, deletion, and size semantics rather than forcing one universal collection interface. Release 0.40.4 changes declarations, correcting what LRUCache.setpop and LRUMap.setpop return. Our complete-package browser build was 101.4 KB minified, so modular import behavior matters.
Our install of Mnemonist 0.40.4 took 0.8 seconds and 1 MB with no audit findings, but an all-exports browser build reached 24.3 KB gzipped. It pays off when one service genuinely needs several unusual structures; use a focused package for one heap, deque, or cache, especially when ESM subpath imports are required.
We installed it
| Install | ✓ · 0.8s | 2 packages on disk · 1 MB |
| Import | ✓ | ESM import works · require() works · CommonJS package with exports map |
| Browser | 24.3 KB | gzipped (101.4 KB minified), bundled with esbuild |
| Types | ✓ | TypeScript types bundled |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does mnemonist install cleanly?
Yes. In a fresh container with an empty cache, npm install mnemonist finished in 0.8s, leaving 2 packages and 1 MB on disk. npm audit reported no known vulnerabilities.
How much does mnemonist add to a browser bundle?
24.3 KB gzipped (101.4 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.
Does mnemonist work with both ESM and CommonJS?
Yes. Both import 'mnemonist' and require('mnemonist') worked in Node 22 in our run. The package is published as CommonJS with an exports map.
Does mnemonist include TypeScript types?
Yes, type declarations ship inside the package, so no @types install is needed.
mnemonist or denque: which should you use?
denque: Choose it when a growable double-ended queue is the whole requirement. Our install of Mnemonist 0.40.4 took 0.8 seconds and 1 MB with no audit findings, but an all-exports browser build reached 24.3 KB gzipped.
When should you not use mnemonist?
Native ESM must import an individual subpath; the exports map supplies subpaths only for require, while root named ESM imports work
Use it if
- One service needs several specialist containers and reviewing one MIT package is preferable to collecting many tiny modules
- A hot path needs a ring buffer, typed vector, bit collection, or sparse set with capacity chosen up front
- Search code needs a typed BK tree, VP tree, SymSpell index, suffix array, or static interval tree
- CommonJS callers can load a single implementation through paths such as require('mnemonist/heap')
- Native ESM must import an individual subpath; the exports map supplies subpaths only for require, while root named ESM imports work
- You need only a production LRU cache with TTL, stale reads, async fetch, size accounting, and disposal; lru-cache owns those policies
- Every structure must use one meaning for size; MultiMap and MultiSet distinguish size from dimension, while BitSet defines size differently
- Overflow must have one policy; FixedDeque throws at capacity but CircularBuffer discards the oldest entry
- The required structure is a graph; Mnemonist's README excludes graphs and points readers to graphology
Setup reality
We installed Mnemonist 0.40.4 in 0.8 seconds on an unprivileged Node 22 container. Two packages used 1 MB afterward. Mnemonist was 648 KB unpacked, declared 1 dependency and 0 peers, bundled its TypeScript declarations, and used the MIT license. npm audit found 0 known vulnerabilities. require() and root-level ESM import both worked through a CommonJS package with an exports map. Importing everything bundled to 101.4 KB minified and 24.3 KB gzipped.
Module format has a sharp edge. require('mnemonist/heap') loads one CommonJS file, but import Heap from 'mnemonist/heap' fails because the wildcard export has no import condition. ESM uses named exports from 'mnemonist'. The package declares no side effects, which helps bundlers discard unused code, yet our 24.3 KB gzip figure shows why a frontend team should inspect its actual output instead of assuming tree shaking happened.
Fixed containers do not share one overflow contract. new CircularBuffer(Array, 100) overwrites its oldest value after 100 entries; a FixedDeque with the same capacity throws. LRUCache and LRUMap also stop at a fixed capacity and have no TTL policy. Their basic forms omit keyed deletion, which is supplied by WithDelete variants. setpop returns an evicted pair only when an insertion displaced something, and 0.40.4 corrected that TypeScript return declaration.
Names must be read per class. MultiSet.size totals occurrences while dimension counts distinct values. DefaultMap.get() inserts a generated default, making an apparent read mutate state. Trie.find() returns every prefix match without a result cap. BloomFilter cannot enumerate or delete values and may say an unseen item is present. BK and VP trees can miss valid neighbors if the supplied distance function violates metric rules.
Patterns
Use the root for ESM and subpaths for CommonJS import-structure
// CommonJS can load one file
const Heap = require('mnemonist/heap');
// ESM uses the package root
import { LRUCache, Trie } from 'mnemonist';Version 0.40.4 exposes named ESM exports only at the root; its wildcard subpaths are require-only.
Pop the lowest-priority number first use-priority-queue
const Heap = require('mnemonist/heap');
const jobs = new Heap((a, b) => a.priority - b.priority);
jobs.push({ id: 'slow', priority: 10 });
jobs.push({ id: 'urgent', priority: 1 });
const next = jobs.pop();
console.log(next.id); // urgentThe comparator follows Array.sort conventions, and this ascending comparison places the smallest priority at the root.
Keep 500 entries by recency cache-recent-values
const LRUCache = require('mnemonist/lru-cache');
const cache = new LRUCache(500);
cache.set('user:7', { id: 7 });
const value = cache.get('user:7'); // refreshes recency
const untouched = cache.peek('user:7');
const present = cache.has('user:7');This cache evicts by fixed count, has no time expiry, and needs LRUCacheWithDelete for explicit keyed removal.
Capture an LRU eviction during insertion observe-lru-eviction
const LRUMap = require('mnemonist/lru-map');
const cache = new LRUMap(2);
cache.set('a', 1);
cache.set('b', 2);
const evicted = cache.setpop('c', 3);
// evicted is ['a', 1]The 0.40.4 declaration describes the pair correctly; setpop returns null when no entry was displaced.
Collect every trie match for a prefix autocomplete-prefix
const TrieMap = require('mnemonist/trie-map');
const words = new TrieMap();
words.set('paper', 1);
words.set('paperback', 2);
words.set('paint', 3);
const matches = words.find('pape');find() has no result limit, so short prefixes over a large dictionary need an application-side bound.
Store distinct values under one key group-multiple-values
const MultiMap = require('mnemonist/multi-map');
const tags = new MultiMap(Set);
tags.set('js', 'post-1');
tags.set('js', 'post-2');
tags.set('js', 'post-2');
console.log(tags.get('js'));
console.log(tags.size, tags.dimension); // 2, 1Set backing removes duplicate post-2, leaving size at 2 while dimension remains the single js key.
Increment and remove occurrence counts count-occurrences
const MultiSet = require('mnemonist/multi-set');
const paths = MultiSet.from(['/a', '/a', '/b']);
paths.add('/b', 2);
console.log(paths.count('/b')); // 3
console.log(paths.top(2));
paths.remove('/a');remove() subtracts one occurrence, delete() drops the whole entry, and top() performs sorting on each call.
Populate a default array on get() create-default-map
const DefaultMap = require('mnemonist/default-map');
const byOwner = new DefaultMap(() => []);
for (const task of tasks) {
byOwner.get(task.owner).push(task);
}
const existing = byOwner.peek('missing');A missing get() mutates this map by storing the factory result; peek() observes without inserting.
Retain only the latest three values keep-recent-window
const CircularBuffer = require('mnemonist/circular-buffer');
const recent = new CircularBuffer(Array, 3);
recent.push('a');
recent.push('b');
recent.push('c');
recent.push('d');
console.log(recent.toArray()); // ['b', 'c', 'd']The fourth push replaces a without warning; FixedDeque is the alternative when reaching capacity must raise.
Rule out an unseen key with a Bloom filter test-probable-membership
const BloomFilter = require('mnemonist/bloom-filter');
const seen = new BloomFilter(100_000);
seen.add('invoice:42');
if (!seen.test('invoice:99')) {
console.log('definitely unseen');
}false proves absence, while true is only probable membership; enumeration and deletion are unavailable.
Union fixed numeric identifiers group-connected-indices
const StaticDisjointSet = require('mnemonist/static-disjoint-set');
const groups = new StaticDisjointSet(6);
groups.union(0, 1);
groups.union(1, 2);
console.log(groups.connected(0, 2)); // true
console.log(groups.dimension); // 4The universe is the integers 0 through 5 here, so strings and sparse IDs need a separate index mapping.
Search a BK tree within distance one search-metric-space
const BKTree = require('mnemonist/bk-tree');
const editDistance = require('./edit-distance');
const tree = BKTree.from(
['book', 'books', 'cake'],
editDistance,
);
const matches = tree.search(1, 'book');Incorrect metric behavior can prune valid matches; returned item-distance objects also have no sorted-order promise.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| denque | npm | Choose it when a growable double-ended queue is the whole requirement. |
| heap-js | npm | Choose it for a focused heap API whose native ESM path is straightforward. |
| lru-cache | npm | Choose it when eviction also needs TTL, stale values, fetch coordination, size limits, or disposal. |
More utils guides
lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

