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 undefinedWeakRef 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'dWeakRef + 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 calledEvent 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 methodsBest 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.