How to Use an LRU Cache with Lodash memoize
By default, Lodash’s _.memoize caches results
indefinitely using an internal Map-like structure, which
can cause significant memory leaks in long-running applications. This
article demonstrates how to replace Lodash's default caching mechanism
with a Least Recently Used (LRU) cache, showing both how to configure it
on individual memoized functions and how to override it globally across
your application.
Understanding Lodash's Cache Contract
Lodash allows you to override its cache implementation because
_.memoize relies on the constructor defined at
_.memoize.Cache. For an LRU cache to work with Lodash, the
cache instance must implement the standard JavaScript Map
interface methods:
has(key): Returns a boolean indicating if the key exists.get(key): Returns the cached value.set(key, value): Stores the value and returns the cache instance.delete(key): Removes an item by key.clear(): Empties the cache.
Popular libraries such as lru-cache implement this
interface directly in recent versions.
Overriding Cache for a Single Function
The safest way to use an LRU cache is on a per-function basis. After
creating a memoized function, you can directly reassign its
.cache property to an instance of your LRU cache before
invoking it.
import _ from 'lodash';
import { LRUCache } from 'lru-cache';
function expensiveComputation(id) {
// Complex operation here
return { id, computedAt: Date.now() };
}
// 1. Memoize the function normally
const memoizedComputation = _.memoize(expensiveComputation);
// 2. Replace the instance's default cache with an LRU cache
memoizedComputation.cache = new LRUCache({
max: 100, // Store a maximum of 100 items
ttl: 1000 * 60 * 5 // Optional: Time to live of 5 minutes
});
// 3. Use as normal
memoizedComputation('user_1');
memoizedComputation('user_1'); // Served from LRU cacheOverriding the Cache Globally
If you want every memoized function created by _.memoize
to use an LRU cache by default, you can override
_.memoize.Cache. Because _.memoize calls
new _.memoize.Cache() without arguments, you should create
a wrapper class that predefines your default LRU cache
configuration.
import _ from 'lodash';
import { LRUCache } from 'lru-cache';
// Create an adapter class with default LRU options
class DefaultLRUCache extends LRUCache {
constructor() {
super({
max: 500 // Maximum items for any memoized function
});
}
}
// Globally override the default cache constructor
_.memoize.Cache = DefaultLRUCache;
// Any new memoized function now uses the LRU cache automatically
const memoizedUserData = _.memoize(fetchUserData);
const memoizedSettings = _.memoize(fetchSettings);
console.log(memoizedUserData.cache instanceof LRUCache); // trueHandling Complex Argument Keys
By default, _.memoize uses the first argument passed to
the function as the cache key. When passing objects or multiple
arguments, provide a resolver function to avoid cache collisions within
your LRU cache:
const memoizedCalculate = _.memoize(
(category, options) => performCalculation(category, options),
(category, options) => `${category}:${JSON.stringify(options)}`
);
memoizedCalculate.cache = new LRUCache({ max: 50 });