WeakRef and FinalizationRegistry let JavaScript hold references to objects without preventing their garbage collection — enabling memory-safe caches, plugin cleanup hooks, and observation patterns that don’t leak memory when WordPress admin components unmount.
Problem: A WordPress admin JavaScript application caches DOM references or API response objects in a Map or plain object — but because the map holds strong references, cached entries for removed DOM nodes or stale data are never garbage-collected, causing a memory leak over time.
Solution: Use WeakRef to hold a non-owning reference to a cached object — the garbage collector can still reclaim it. Pair with FinalizationRegistry to receive a callback when the referenced object is collected, enabling automatic cache eviction. This pattern is ideal for large object caches keyed by DOM nodes.
The examples below build a WeakRef-based DOM element cache that automatically clears when elements are removed, use FinalizationRegistry to log when a block editor component is garbage-collected, and implement a memory-safe event listener cleanup registry.
// WeakRef — holds a reference to an object but does not prevent GC
// .deref() returns the object if still alive, or undefined if collected
// Memory-safe DOM cache: holds element references weakly
// When the element is removed from the DOM and GC'd, the cache entry vanishes automatically
const elementCache = new Map(); // Note: WeakMap would be even better here
function getOrCreateTooltip( triggerEl ) {
const ref = elementCache.get( triggerEl );
const existing = ref?.deref(); // deref() returns undefined if GC'd
if ( existing ) return existing;
const tooltip = document.createElement( 'div' );
tooltip.className = 'wp-tooltip';
document.body.appendChild( tooltip );
// Store as a WeakRef — tooltip can still be GC'd if removed from DOM
elementCache.set( triggerEl, new WeakRef( tooltip ) );
return tooltip;
}
// WeakMap is the preferred tool when the key IS the object you want to track:
const blockData = new WeakMap();
function attachBlockMeta( blockEl, meta ) {
blockData.set( blockEl, meta ); // no memory leak — GC'd with the element
}
function getBlockMeta( blockEl ) {
return blockData.get( blockEl ); // undefined if GC'd or never set
}
Use FinalizationRegistry to clean up resources when objects are garbage-collected:
// FinalizationRegistry — runs a callback when a registered object is GC'd
// Useful for closing WebSockets, cancelling intervals, or logging leaks
const registry = new FinalizationRegistry( ( heldValue ) => {
// heldValue is the "token" you passed at registration time
// The registered target object is ALREADY GONE when this fires
console.log( `Block component "${heldValue}" was garbage collected` );
// Clean up associated resources using the token (not the dead object)
openConnections.delete( heldValue );
clearInterval( pollingIntervals.get( heldValue ) );
} );
// Register a block editor component instance for cleanup tracking
function createBlockComponent( clientId ) {
const component = {
clientId,
ws: new WebSocket( 'wss://example.com/live' ),
};
// Register component — second arg is the "held value" (survives GC)
registry.register( component, clientId );
openConnections.set( clientId, component.ws );
return component;
}
// Important caveats:
// 1. GC timing is non-deterministic — do NOT rely on FinalizationRegistry for
// time-sensitive cleanup. Use explicit cleanup functions for that.
// 2. Callbacks may not fire at all in some environments (e.g., tests).
// 3. WeakRef.deref() can return undefined at any point after GC is eligible.
// → Use these APIs for optional optimisations, not critical logic.
NOTE: WeakRef and FinalizationRegistry are supported in all modern browsers and Node.js 14+. The most practical use case in WordPress JavaScript is a WeakMap-based metadata store attached to DOM elements or Gutenberg block objects — it provides O(1) lookup without any manual cleanup code.