JS: All You Can Weak!

Andrea Giammarchi
6 min readAug 12, 2021
Photo by Anant Chandra on Unsplash

You might already know both WeakMap and WekSet, but many don’t know yet either WeakRef, or the mighty FinalizationRegistry, which are enablers of new, unthinkable, patterns!

This post goal is to describe all these modern primitives, and their use cases.

Once upon a time …

… we used attach any kind of property directly to objects and/or DOM nodes (also known as expando), likely polluting these objects in the wild, compromising their runtime “shape”, simply to relate any complex value to another.

The only use case, and old time feature, provided by this technique, is that once the generic object gets garbage collected, all these properties would fade away with it … let’s see an example:

var secret = 'secret-prop' + Math.random();
function init(object) {
if (!(secret in object)) {
object[secret] = Date.now();
// do something else once
}
}

However, initializing some object, enriching some other, handling foreign states, “private” properties, and whatsoever, with this pattern, is very error-prone obtrusive:

  • error prone because any shallow or deep copy of the object would’ve leaked the “secret” properties
  • obtrusive because the property would’ve been attached to an object that, potentially, we don’t own at all

Of course …”, one might think, “… that’s a bad practice indeed, which is why we use Symbol instead!

No … not Symbol …

The only real-world problem that Symbol solves, is the name-clashing nature of each property, whenever it’s been created through Symbol(name), as opposite of Symbol.for(name) … although, everything else is the same:

  • Symbols leak when objects are copied
  • Symbols are by default enumerable and easily discoverable
  • Symbols attached at runtime are still error prone and obtrusive

On top of all these points, neither Symbols nor generic properties, attached directly, or through Object.defineProperty, are capable of relating (frozen, sealed, etc.) immutable, objects, to anything else, so that we’re out of luck, unless we use the right tool for the job.

WeakMap

Whenever we don’t know, or control, the lifecycle of a generic object, WeakMaps allow us to relate even immutable objects to any sort of data:

const wm = new WeakMap;
function init(object) {
if (!wm.has(object)) {
wm.set(object, Date.now());
// do something else once
}
}

The benefits of using a WeakMap in a nutshell:

  • no leak whatsoever even once the object has been deeply cloned
  • never obtrusive: frozen, sealed, prevented-extension, and DOM objects, can be used as key, like any other non-primitive reference
  • whenever the object is garbage collected, related value will be collected too, if possible

What many also don’t know, is that WeakMap works in IE11 too, and it’s so handy, that it’s used by Babel to transpile private classes properties and methods.

Strawberry on top, WeakMaps are also extremely fast!

WeakSet

Similarly to WeakMaps, WeakSets let us keep track of a set of generic objects, without holding their references in memory. Going back to the init example, if no specific property needs to be related, a WeakSet would do an even better job:

const ws = new WeakSet;
function init(object) {
if (!ws.has(object)) {
ws.add(object);
// add listeners, notify, or do something
// related to object as now known reference
}
}

Use cases for WeakSets are various, including:

  • be sure a specific DOM element hasn’t been gracefully enhanced already
  • be sure a generic object is not considered more than once during some heavy synchronous, or asynchronous, operation
  • every other Set operation that doesn’t expire “right after the loop

… but IE11 …

Beside suggesting the @ungap/weakset polyfill, as module, the smallest “poorlyfill” for IE11 I could think about, is literally 10 lines of code:

self.WeakSet || (self.WeakSet = function () {
var wm = new WeakMap;
return {
add: function (key) {
return wm.set(key, true), this;
},
delete: wm.delete.bind(wm),
has: wm.has.bind(wm),
};
});

Enough IE11 though

The rest of this articles covers latest primitives to deal with weakly referenced values, so that if you’re stuck in supporting this browser from 2013, there is no way you can polyfill these features, hence feel free to stop reading 👋

WeakRef

From the original proposal itself:

A primary use for weak references is to implement caches or mappings holding large objects, where it’s desired that a large object is not kept alive solely because it appears in a cache or mapping.

The whole documentation around this primitive kinda scares developers away with warnings, “be careful” notes, and so on. The truth is that WeakRefs allow us to invert the key/value relation we have with WeakMaps, so that the key can be any primitive, or reference, while the value will be the weakly related part.

The weak-value module is an excellent micro-utility that solves that problem only, and in an appropriate way, but let’s see a row example of what’s a WeakRef about:

const images = new Map;
const getImage = async (path) => {
if (images.has(path)) {
const image = images.get(path).deref();
// true only if not collected yet
if (image)
return image;
}
const image = new Image;
const ref = new WeakRef(image);
images.set(path, ref);
return new Promise((ok, err) => {
image.onload = () => {
ok(image);
};
image.onerror = err;
});
};

In this code, images might be reused, loaded, or discarded, at any time, and the WeakRef wrapper would hint to reload these, if not available yet, or anymore.

The tiny caveat, and the reason there are so many “be careful” around this primitive, is that “smart code” like the one I’ve just written, easily leaks in the wild, never freeing, as example, all the related Map’s keys, unless some part of the code keep asking for all of them, and we check that all references are valid and free, if not, those slots for the future.

These kind of caveats are the reason WeakRefs are mostly useful only if combined with a FinalizationRegistry around, in charge of automatically cleanup those keys, also properly implemented in the previously mentioned weak-value module.

FinalizationRegistry

Probably due to the fact that many developers got excited about WeakRef only, which is mostly useful only with a FinalizationRegistry around, this new wonder in the JS’ landscape has been probably underestimated in terms of potentials.

Dare I say, in an ideal world, where both WeakRef and FinalizationRegistry primitives, together with private class fields, were already available in ES2015 specs, WeakMap and WeakSet could’ve been created with relative ease … example:

class WeakSet {
#set = new Set;
#delete = value => {
for (const ref of this.#set.values()) {
if (ref.deref() === value) {
this.#set.delete(ref);
this.#registry.unregister(value);
}
}
};
#registry = new FinalizationRegistry(value => {
for (const ref of this.#set.values()) {
if (ref === value)
this.#set.delete(ref);
}
});
add(value) {
this.#delete(value);
const ref = new WeakRef(value);
this.#registry.register(value, ref);
this.#set.add(ref);
return this;
}
delete(value) {
return this.has(value) && !this.#delete(value);
}
has(value) {
for (const ref of this.#set.values()) {
if (ref.deref() === value)
return true;
}
return false;
}
}

Not only WeakRef though …

What is extremely important to understand behind the FinalizationRegistry primitive, is that its weakly referenced super-powers are not confined around the usage of the WeakRef primitive, and just work, out of the box, with literally everything.

As example, the proxied-worker module provides a remotely driven instances approaches, through unique IDs defined on the client, and a real instance created on the Worker.

const remoteInstance = await new RemoteClass(1, 2, 3);

When this happens, the logic behind the scene simply returns a Proxy able to drive, through a postMessage(...) dance, an instance created on the Worker side, so that whenever that proxy gets garbage collected, the client’s FinalizationRegistry invokes a postMessage(uid) that cleans up, on the Worker side, the associated instance, and this has never been seen before in JS, but it’s the ABC to avoid memory leaks on remotely driven environments.

Conclusions

Beside being about the time to ditch not ever-green browsers, JS is evolving in a way that lets us fine-tune heap and memory consumption through simple, handy, yet powerful, APIs, and the proxied-worker module is likely just the tip of the iceberg these primitives unlock, so please explore, and create, new patterns, utilities, or libraries, able to avoid memory leaks, in what’s likely the most evolved scripting language to date: JavaScript ♥

--

--

Andrea Giammarchi

Web, Mobile, IoT, and all JS things since 00's. Formerly JS engineer at @nokia, @facebook, @twitter.