Back to Blog
JavaScriptWeakRefFinalizationRegistryMemory Management

JavaScript WeakRef and FinalizationRegistry Guide

Master JavaScript WeakRef and FinalizationRegistry for advanced memory management patterns.

B
Bootspring Team
Engineering
July 26, 2019
6 min read

WeakRef and FinalizationRegistry provide advanced memory management capabilities. Here's how to use them responsibly.

Basic WeakRef#

1// Create a weak reference 2const target = { data: 'important' }; 3const weakRef = new WeakRef(target); 4 5// Access the target 6const value = weakRef.deref(); 7if (value) { 8 console.log(value.data); // 'important' 9} else { 10 console.log('Object was garbage collected'); 11} 12 13// After target goes out of scope and GC runs, 14// deref() may return undefined

WeakRef for Caching#

1class WeakCache { 2 constructor() { 3 this.cache = new Map(); 4 } 5 6 set(key, value) { 7 this.cache.set(key, new WeakRef(value)); 8 } 9 10 get(key) { 11 const ref = this.cache.get(key); 12 if (!ref) return undefined; 13 14 const value = ref.deref(); 15 if (!value) { 16 // Clean up dead reference 17 this.cache.delete(key); 18 return undefined; 19 } 20 21 return value; 22 } 23 24 has(key) { 25 return this.get(key) !== undefined; 26 } 27} 28 29// Usage 30const cache = new WeakCache(); 31 32function processData(id) { 33 let data = cache.get(id); 34 35 if (!data) { 36 data = fetchExpensiveData(id); 37 cache.set(id, data); 38 } 39 40 return data; 41}

FinalizationRegistry#

1// Create registry with cleanup callback 2const registry = new FinalizationRegistry((heldValue) => { 3 console.log(`Object with id ${heldValue} was garbage collected`); 4 // Perform cleanup 5}); 6 7// Register an object 8const obj = { data: 'example' }; 9registry.register(obj, 'object-123'); // 'object-123' is held value 10 11// Later, when obj is garbage collected, 12// the callback will be called with 'object-123'

Resource Cleanup Pattern#

1class ResourceManager { 2 constructor() { 3 this.resources = new Map(); 4 this.registry = new FinalizationRegistry((resourceId) => { 5 this.cleanup(resourceId); 6 }); 7 } 8 9 allocate(owner) { 10 const resourceId = crypto.randomUUID(); 11 const resource = createResource(resourceId); 12 13 this.resources.set(resourceId, resource); 14 this.registry.register(owner, resourceId); 15 16 return resource; 17 } 18 19 cleanup(resourceId) { 20 const resource = this.resources.get(resourceId); 21 if (resource) { 22 resource.close(); 23 this.resources.delete(resourceId); 24 console.log(`Cleaned up resource ${resourceId}`); 25 } 26 } 27} 28 29// Usage 30const manager = new ResourceManager(); 31 32function processWithResource() { 33 const owner = {}; 34 const resource = manager.allocate(owner); 35 36 // Use resource... 37 return result; 38 39 // When owner is GC'd, resource is automatically cleaned up 40}

Unregister Token#

1const registry = new FinalizationRegistry((heldValue) => { 2 console.log(`Cleanup: ${heldValue}`); 3}); 4 5const obj = { data: 'test' }; 6const unregisterToken = {}; 7 8// Register with unregister token 9registry.register(obj, 'my-object', unregisterToken); 10 11// Later, manually unregister 12registry.unregister(unregisterToken); 13// Now cleanup won't be called when obj is GC'd

WeakRef + FinalizationRegistry#

1class SmartCache { 2 constructor() { 3 this.cache = new Map(); 4 this.registry = new FinalizationRegistry((key) => { 5 // Clean up the WeakRef entry when object is collected 6 this.cache.delete(key); 7 console.log(`Cache entry ${key} cleaned up`); 8 }); 9 } 10 11 set(key, value) { 12 // If replacing, unregister old value 13 const existing = this.cache.get(key); 14 if (existing) { 15 this.registry.unregister(existing); 16 } 17 18 const ref = new WeakRef(value); 19 this.cache.set(key, ref); 20 this.registry.register(value, key, ref); 21 } 22 23 get(key) { 24 const ref = this.cache.get(key); 25 return ref?.deref(); 26 } 27 28 // Explicit cleanup 29 delete(key) { 30 const ref = this.cache.get(key); 31 if (ref) { 32 this.registry.unregister(ref); 33 this.cache.delete(key); 34 } 35 } 36}

DOM Element Tracking#

1class ElementTracker { 2 constructor() { 3 this.elements = new Map(); 4 this.registry = new FinalizationRegistry((elementId) => { 5 console.log(`Element ${elementId} was removed and GC'd`); 6 this.elements.delete(elementId); 7 this.onElementRemoved?.(elementId); 8 }); 9 } 10 11 track(element, id) { 12 this.elements.set(id, new WeakRef(element)); 13 this.registry.register(element, id); 14 } 15 16 getElement(id) { 17 return this.elements.get(id)?.deref(); 18 } 19 20 onElementRemoved = null; 21} 22 23// Usage 24const tracker = new ElementTracker(); 25tracker.onElementRemoved = (id) => { 26 // Update UI, clear related data, etc. 27}; 28 29const div = document.createElement('div'); 30tracker.track(div, 'my-div'); 31 32// Later, when div is removed and GC'd, 33// onElementRemoved is called

Event Listener Management#

1class WeakEventEmitter { 2 constructor() { 3 this.listeners = new Map(); 4 this.registry = new FinalizationRegistry(({ event, listenerId }) => { 5 this.removeListener(event, listenerId); 6 }); 7 } 8 9 on(event, listener, context) { 10 if (!this.listeners.has(event)) { 11 this.listeners.set(event, new Map()); 12 } 13 14 const listenerId = crypto.randomUUID(); 15 const entry = { 16 listener, 17 contextRef: context ? new WeakRef(context) : null, 18 }; 19 20 this.listeners.get(event).set(listenerId, entry); 21 22 if (context) { 23 this.registry.register(context, { event, listenerId }); 24 } 25 26 return listenerId; 27 } 28 29 emit(event, ...args) { 30 const eventListeners = this.listeners.get(event); 31 if (!eventListeners) return; 32 33 for (const [id, { listener, contextRef }] of eventListeners) { 34 const context = contextRef?.deref() ?? null; 35 36 // Skip if context was GC'd 37 if (contextRef && !context) { 38 eventListeners.delete(id); 39 continue; 40 } 41 42 listener.call(context, ...args); 43 } 44 } 45 46 removeListener(event, listenerId) { 47 this.listeners.get(event)?.delete(listenerId); 48 } 49}

Image Cache Example#

1class ImageCache { 2 constructor(maxSize = 100) { 3 this.cache = new Map(); 4 this.maxSize = maxSize; 5 this.registry = new FinalizationRegistry((url) => { 6 console.log(`Image GC'd: ${url}`); 7 this.cache.delete(url); 8 }); 9 } 10 11 async get(url) { 12 const cached = this.cache.get(url)?.deref(); 13 if (cached) { 14 return cached; 15 } 16 17 // Load image 18 const image = await this.loadImage(url); 19 20 // Evict if cache is full 21 if (this.cache.size >= this.maxSize) { 22 const firstKey = this.cache.keys().next().value; 23 this.cache.delete(firstKey); 24 } 25 26 this.cache.set(url, new WeakRef(image)); 27 this.registry.register(image, url); 28 29 return image; 30 } 31 32 async loadImage(url) { 33 return new Promise((resolve, reject) => { 34 const img = new Image(); 35 img.onload = () => resolve(img); 36 img.onerror = reject; 37 img.src = url; 38 }); 39 } 40}

Object Pool with Cleanup#

1class ObjectPool { 2 constructor(factory, reset, maxSize = 10) { 3 this.factory = factory; 4 this.reset = reset; 5 this.maxSize = maxSize; 6 this.available = []; 7 this.inUse = new Set(); 8 this.registry = new FinalizationRegistry((obj) => { 9 // Object was GC'd without being returned 10 console.warn('Object not returned to pool, was GC\'d'); 11 this.inUse.delete(obj); 12 }); 13 } 14 15 acquire() { 16 let obj; 17 18 if (this.available.length > 0) { 19 obj = this.available.pop(); 20 } else { 21 obj = this.factory(); 22 } 23 24 this.inUse.add(obj); 25 this.registry.register(obj, obj); 26 27 return obj; 28 } 29 30 release(obj) { 31 if (!this.inUse.has(obj)) { 32 return; 33 } 34 35 this.registry.unregister(obj); 36 this.inUse.delete(obj); 37 this.reset(obj); 38 39 if (this.available.length < this.maxSize) { 40 this.available.push(obj); 41 } 42 } 43} 44 45// Usage 46const bufferPool = new ObjectPool( 47 () => new ArrayBuffer(1024), 48 (buf) => new Uint8Array(buf).fill(0) 49);

Important Caveats#

1// ⚠️ WARNING: Timing is unpredictable 2// GC timing is not guaranteed, don't rely on it for correctness 3 4// ❌ BAD: Relying on finalization for critical cleanup 5class BadExample { 6 constructor() { 7 this.file = openFile(); 8 registry.register(this, this.file); 9 } 10 // File might stay open indefinitely! 11} 12 13// ✅ GOOD: Use explicit cleanup with finalization as backup 14class GoodExample { 15 constructor() { 16 this.file = openFile(); 17 registry.register(this, this.file, this); 18 } 19 20 close() { 21 if (this.file) { 22 this.file.close(); 23 this.file = null; 24 registry.unregister(this); 25 } 26 } 27} 28 29// Always prefer: 30// - try/finally 31// - using declarations (when available) 32// - explicit dispose/close methods

Best Practices#

Valid Use Cases: ✓ Caches that can lose entries ✓ Tracking external resources ✓ Secondary cleanup mechanism ✓ Debugging memory leaks Requirements: ✓ Must work if cleanup never happens ✓ Must work if cleanup is delayed ✓ Primary cleanup should be explicit ✓ Finalization is a fallback Performance: ✓ WeakRef.deref() is fast ✓ Don't create many WeakRefs ✓ Clean up dead refs periodically ✓ Avoid in hot paths Avoid: ✗ Critical resource cleanup ✗ Side effects with timing requirements ✗ Assuming GC timing ✗ Complex finalization logic

Conclusion#

WeakRef and FinalizationRegistry enable advanced memory management patterns like weak caches and automatic resource cleanup. WeakRef holds references without preventing garbage collection, while FinalizationRegistry runs callbacks after objects are collected. Always design systems to work correctly without relying on finalization timing, and use these APIs as optimization or fallback mechanisms, not for critical cleanup logic.

Share this article

Help spread the word about Bootspring