How to Cache Assets with JavaScript WeakRef
JavaScript’s WeakRef object allows developers to
maintain references to large assets—such as parsed data sets, images, or
audio buffers—without preventing the garbage collector from freeing that
memory when system resources become constrained. In traditional caching
mechanisms, standard variable assignments and data structures create
strong references that keep objects alive in memory indefinitely. By
wrapping cached values in a WeakRef, an application can
quickly reuse assets that remain in memory while granting the engine
full permission to reclaim that memory whenever necessary.
The Problem with Strong Reference Caching
Standard JavaScript objects, arrays, and collections like
Map create strong references. As long as an asset is
referenced inside a cache, the garbage collector considers it reachable
and will never free its memory. In applications handling heavy
resources, this behavior can quickly lead to high memory consumption,
degraded performance, or browser crashes unless manual cache eviction
strategies (such as LRU algorithms) are carefully managed.
How WeakRef Solves the Issue
A WeakRef holds a reference to a target object without
making it strongly reachable. If an object has no remaining strong
references pointing to it, the JavaScript garbage collector is allowed
to destroy the object and reclaim its memory, even if one or more
WeakRef instances still point to it.
To retrieve an object from a weak reference, you call its
.deref() method:
- If the asset is still in memory:
.deref()returns the target object, allowing the application to reuse the cached data instantly. - If the asset was collected:
.deref()returnsundefined, signaling that the application must reload, recompute, or re-fetch the asset.
Implementing a WeakRef Asset Cache
A typical pattern involves pairing a standard Map with
WeakRef wrappers to store assets alongside their retrieval
keys:
class AssetCache {
constructor() {
this.cache = new Map();
}
get(key) {
const ref = this.cache.get(key);
if (ref) {
const cachedAsset = ref.deref();
if (cachedAsset !== undefined) {
return cachedAsset; // Cache hit: Asset is still in memory
}
}
return null; // Cache miss or asset was garbage collected
}
set(key, asset) {
this.cache.set(key, new WeakRef(asset));
}
}When an asset is requested via get(key): 1. The cache
checks if a WeakRef exists for the given key. 2. It
attempts to resolve the asset using .deref(). 3. If the
asset exists, it is returned without the overhead of re-fetching. 4. If
.deref() yields undefined, the application
fetches the original asset again and stores a new
WeakRef.
Cleaning Up Stale Keys with FinalizationRegistry
While WeakRef allows the asset payload to be reclaimed,
the string key and the WeakRef wrapper itself remain inside
the Map. To clean up empty cache entries automatically,
WeakRef can be combined with
FinalizationRegistry.
A FinalizationRegistry allows you to register a callback
that runs after a target object has been garbage collected, passing a
held value (such as the cache key) to clean up stale entries from the
tracking Map:
class SelfCleaningCache {
constructor() {
this.cache = new Map();
this.registry = new FinalizationRegistry((key) => {
// Clean up the key if the corresponding asset was collected
const ref = this.cache.get(key);
if (ref && ref.deref() === undefined) {
this.cache.delete(key);
}
});
}
set(key, asset) {
this.cache.set(key, new WeakRef(asset));
this.registry.register(asset, key);
}
get(key) {
const ref = this.cache.get(key);
return ref ? ref.deref() : undefined;
}
}Important Considerations
- Non-deterministic Timing: Garbage collection timing depends entirely on the JavaScript engine implementation and available host memory. An unreferenced asset might be cleared immediately or persist for a long time.
- Fallback Logic is Mandatory: Code interacting with
WeakRefmust always implement fallback logic to reconstruct or re-fetch assets when.deref()returnsundefined. - Objects Only:
WeakRefcan only hold references to objects, not primitive values like numbers or strings.