Lodash Memoize: Preventing Cache Memory Leaks
By default, the Lodash _.memoize function provides no
built-in automatic eviction mechanisms—such as Time-to-Live (TTL)
expiration or Least Recently Used (LRU) clearing—to prevent memory
leaks. Because its internal cache retains arguments and return values
indefinitely, developers must rely on built-in extension points to
manage memory. This article explains how the Lodash caching system
operates and outlines the specific mechanisms available to prevent
unbounded memory growth, including manual cache invalidation, custom
cache constructors, and WeakMap implementations.
The Default Cache Behavior
When a function is wrapped with _.memoize, Lodash
creates an internal cache store using _.memoize.Cache,
which defaults to JavaScript’s native Map (falling back to
a polyfilled object map in older environments). Every unique computed
key adds an entry to this map.
Because standard Map instances hold strong references to
both keys and values, the garbage collector cannot reclaim cached data,
even if the application no longer requires it. Without intervention,
long-running processes or high-cardinality inputs will cause the memory
footprint to expand continually.
Manual Cache Invalidation
Lodash explicitly attaches the underlying storage instance to the
memoized function via the cache property. This mechanism
allows developers to inspect, modify, or completely clear stored entries
manually:
- Clearing the entire cache: Calling
memoizedFunction.cache.clear()invokes the underlyingMap.prototype.clear()method, wiping out all stored results and freeing associated memory. - Deleting specific entries: Individual keys can be
removed using
memoizedFunction.cache.delete(key)when certain data becomes stale.
While manual invalidation allows programmatic control, it requires external triggers (such as timers, events, or lifecycle hooks) to manage memory cleanup actively.
Custom Cache Constructors
The primary architectural mechanism Lodash provides to avert memory
leaks is the ability to override the default cache constructor. The
_.memoize.Cache property can be replaced globally, or a
custom instance can be attached to a specific memoized function.
To implement bounded caching, developers can substitute an LRU cache
or a fixed-size cache library that adheres to the Map
interface (implementing has, get,
set, and delete):
const LRU = require('lru-cache');
// Configure a bounded cache implementation
_.memoize.Cache = class {
constructor() {
this.cache = new LRU({ max: 500 });
}
has(key) { return this.cache.has(key); }
get(key) { return this.cache.get(key); }
set(key, val) { this.cache.set(key, val); return this; }
delete(key) { return this.cache.delete(key); }
clear() { this.cache.clear(); }
};Using a bounded cache limits the maximum number of items in memory, automatically evicting older entries when new ones arrive.
WeakMap for Object Keys
If the memoized function exclusively accepts objects as cache keys,
the cache constructor can be replaced with native WeakMap.
A WeakMap maintains "weak" references to its keys, meaning
that if an object key has no other references remaining elsewhere in the
application, the garbage collector will automatically collect both the
key and the cached value. This approach entirely eliminates memory leaks
for object-based lookups without requiring manual cache management.
Key Resolution Control
By default, Lodash only uses the first argument passed to the
memoized function as the cache key. Lodash allows a custom
resolver function as a second parameter to
_.memoize. A poorly written resolver can generate an
excessive number of unique string keys, accelerating memory exhaustion.
By defining strict, controlled resolver functions, developers can limit
the creation of redundant entries and keep the overall cache footprint
predictable.