weak-lru-cache review
weak-lru-cache 1.2.2 is an in-process JavaScript cache with two retention phases. A least-recently and frequently used expirer first holds values strongly. After an object leaves that window, a `WeakRef` can still return it until garbage collection reclaims it; a `FinalizationRegistry` later removes the dead entry. This preserves object identity while memory allows, but it offers no TTL, persistence, cross-process sharing, byte ceiling, or predictable eviction time. Primitive values cannot enter the weak phase and disappear when the strong expirer removes them. The current version remains the February 2022 release.
weak-lru-cache 1.2.2 installed in 0.5 seconds, used 1 MB, passed npm audit, and bundled to 1.5 KB gzipped in our sandbox. Use it only for recreatable object identities that may vanish whenever GC chooses; ordinary application caches should prefer maintained TTL and capacity controls from `lru-cache`.
We installed it
| Install | ✓ · 0.5s | 1 package on disk · 1 MB |
| Import | ✓ | ESM import works · require() works · ESM package with exports map |
| Browser | 1.5 KB | gzipped (3.7 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 weak-lru-cache install cleanly?
Yes. In a fresh container with an empty cache, npm install weak-lru-cache finished in 0.5s, leaving 1 package and 1 MB on disk. npm audit reported no known vulnerabilities.
How much does weak-lru-cache add to a browser bundle?
1.5 KB gzipped (3.7 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.
Does weak-lru-cache work with both ESM and CommonJS?
Yes. Both import 'weak-lru-cache' and require('weak-lru-cache') worked in Node 22 in our run. The package is published as ESM with an exports map.
Does weak-lru-cache include TypeScript types?
Yes, type declarations ship inside the package, so no @types install is needed.
weak-lru-cache or lru-cache: which should you use?
lru-cache: Use it for maintained deterministic limits, TTLs, disposal hooks, and async fetch support. weak-lru-cache 1.2.2 installed in 0.5 seconds, used 1 MB, passed npm audit, and bundled to 1.5 KB gzipped in our sandbox.
When should you not use weak-lru-cache?
Expiry must happen after a known duration. This package has no TTL or maximum age; garbage collection timing belongs to the runtime.
Use it if
- Cached values are recreatable objects and a miss at any later read is always safe.
- Callers benefit from receiving the same object identity while another reference or the cache keeps that object alive.
- The runtime supports `WeakRef` and `FinalizationRegistry`, and nondeterministic garbage collection is part of the design.
- Memory pressure should reclaim cold objects without a wall-clock TTL or application-managed disposal schedule.
- Expiry must happen after a known duration. This package has no TTL or maximum age; garbage collection timing belongs to the runtime.
- Values are mostly strings, numbers, booleans, or null. Primitives cannot be weakly referenced and are dropped after strong retention ends.
- Capacity must be an exact object or byte limit. `cacheSize` tunes LRFU storage and says nothing exact about weakly reachable objects.
- Generic Map code will consume the cache. Inherited `get()` returns an internal `CacheEntry`; application values require `getValue()`.
- Current releases and exact types are required. Version 1.2.2 dates to 2022, and `setValue()` returns an entry at runtime while its declaration says `void`.
Setup reality
We installed weak-lru-cache 1.2.2 in a fresh Node 22 Bookworm sandbox in 0.5 seconds. It left 1 package and 1 MB on disk. npm audit found 0 known vulnerabilities. The ESM package has no direct or peer dependencies, is 64 KB unpacked, ships TypeScript declarations, and uses an exports map. Both CommonJS require() and ESM import worked on Node 22.23.2. Our browser build measured 3.7 KB minified and 1.5 KB gzipped.
There are no credentials, native builds, services, or configuration files. Runtime support is the first gate: the README specifies Node 14.10 or newer because construction uses WeakRef and FinalizationRegistry. WeakLRUCache extends Map, but its values are internal cache entries. Use setValue() and getValue() for application data. Iteration and inherited get() expose entries; inherited set() expects an entry. The bundled declaration says setValue returns void, while the 1.2.2 source returns the created CacheEntry.
Objects begin with a strong reference in the shared LRFU expirer. Once expired there, the entry keeps a weak reference and getValue() may still recover the object until GC runs. The default cacheSize is 32,768 and the documented maximum is 16,777,216, but this sizes the strong retention machinery rather than setting an exact number of live objects. A higher expirationPriority makes an entry leave sooner; -1 pins it in strong memory. Pinning defeats the memory-release reason for this cache.
The default expirer is shared across cache instances, so heavily used caches compete differently from isolated ones. Pass expirer: false for weak-only behavior or construct LRFUExpirer when isolation is intentional. Tests must never require GC to run by a deadline. A read can miss even after an earlier write, and concurrent misses can duplicate reconstruction work. Use explicit delete() for invalidation and clear() for teardown; both remove entries from the expirer instead of waiting for finalization.
Patterns
Store and retrieve one object cache-object
import { WeakLRUCache } from 'weak-lru-cache';
const cache = new WeakLRUCache();
cache.setValue('user:42', { id: 42, name: 'Ada' });
const user = cache.getValue('user:42');The object starts strongly retained, then becomes weakly reachable after LRFU expiry and may disappear after garbage collection.
Rebuild data after a miss fill-on-miss
async function getUser(id) {
const key = `user:${id}`;
let user = cache.getValue(key);
if (user === undefined) {
user = await database.loadUser(id);
cache.setValue(key, user);
}
return user;
}A miss can occur after any earlier write. Two concurrent misses may issue 2 database reads because loading is not coalesced.
Reuse an object's identity intern-object
function internNode(id, fields) {
const existing = cache.getValue(id);
if (existing) return existing;
const node = Object.freeze({ id, ...fields });
cache.setValue(id, node);
return node;
}Identity lasts only while the object remains alive. After GC, the same key can produce a newly constructed object.
Tune strong retention set-cache-size
const cache = new WeakLRUCache({
cacheSize: 4096,
});A 4,096 entry setting sizes the LRFU structure. It is not an exact cap on weakly reachable objects or bytes.
Expire a large value sooner weight-large-object
const estimatedBytes = Buffer.byteLength(JSON.stringify(document));
cache.setValue(document.id, document, estimatedBytes >> 10);Higher priority expires sooner. The README suggests approximate kilobytes as a relative weight, not byte-accurate memory accounting.
Pin one object strongly pin-value
cache.setValue('schema', compiledSchema, -1);A negative priority pins the object until the entry changes. One pin removes GC's ability to reclaim that cached value.
Return a pin to normal retention unpin-value
const schema = cache.getValue('schema');
if (schema) cache.setValue('schema', schema, 0);Reading a pinned entry keeps it pinned. Replacing it with priority 0 sends it back through normal expiration.
Disable the strong expirer use-weak-only
const weakOnly = new WeakLRUCache({
expirer: false,
});
weakOnly.setValue('preview', previewObject);Without strong retention, the value may become collectible as soon as outside references disappear. Primitives have no useful weak phase.
Create a private retention policy isolate-expirer
import { LRFUExpirer, WeakLRUCache } from 'weak-lru-cache';
const expirer = new LRFUExpirer({
lruSize: 2048,
cleanupInterval: 30_000,
});
const isolated = new WeakLRUCache({ expirer });The default expirer is shared. A private 2,048-slot generation isolates competition but gives up cross-cache prioritization.
Invalidate one key immediately delete-entry
const removed = cache.delete('user:42');
console.log({ removed });`delete` removes the Map entry and its LRFU reference. Use it for correctness instead of waiting for GC.
Remove every cached entry clear-cache
cache.clear();`clear` walks all entries and detaches them from the expirer. Other application references can still keep those objects alive.
Access entry metadata deliberately inspect-entry
const entry = cache.get('user:42');
const value = entry?.deref ? entry.deref() : entry?.value;Inherited `get()` returns a `CacheEntry`, not the application value. Ordinary reads should call `getValue()` instead.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| lru-cache | npm | Use it for maintained deterministic limits, TTLs, disposal hooks, and async fetch support. |
| quick-lru | npm | Use it for a small ESM LRU with a predictable maximum entry count. |
| mnemonist | npm | Use it when a broader collection package with fixed-capacity LRU structures is useful. |
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.

