Skip to content

Lookup table with weak references in Javascript

I have a tree structure with elements dynamically added and removed. The elements are loaded dynamically from the network. What I want to achieve is to have a lookup table that maps the element’s id to the actual element in the tree. Now, the problem when using a simple Map or Object is that it holds strong references to the tree elements which will bloat the memory after a while. Since node >= 14.6.0 and Chrome >= 84 supposedly support WeakRef’s I thought I could make a Map that holds WeakRefs to my tree elements and then simply deref() and see if the elements are still around. I tried to test this but it doesn’t seem to work. My minimal test looks like this:

const lookup = new Map();
let element = new Object({id:"someid", data: {}});

lookup.set(element.id, new WeakRef(element));
console.dir(lookup.get("someid").deref());
// as expected output is { id: 'someid', data: {} }

element = null;
console.log(element);
// as expected output is null

// simply calling global.gc() didn't work
// so i made this loop which allocates mem *and* calls global.gc() to
// force garbage collection
// problem: infinite loop because deref always returns the dereferenced
// value which should have gone since element was set to null

while (lookup.get("someid").deref()) {
  const a = new Array(1000);
  // enabled with --expose-gc in node
  global.gc();
}

console.dir(lookup.get("someid").deref());

As written above in the comment, the issue is that the loop never ends because the deref call always returns a value despite the element var being set to null.

Am I missing something here? If not and this is how it is supposed to work, how can I achieve my goal of having a map of weak references (WeakMap is not an option here, since I would have an O(n) cost of looking up an element by id)?.

Answer

Am I missing something here?

Yes: you’re missing the notes in the documentation you’ve linked to, for instance:

If your code has just created a WeakRef for a target object, or has gotten a target object from a WeakRef’s deref method, that target object will not be reclaimed until the end of the current JavaScript job (including any promise reaction jobs that run at the end of a script job). That is, you can only “see” an object get reclaimed between turns of the event loop.

And of course:

Avoid where possible
Correct use of WeakRef takes careful thought, and it’s best avoided if possible. It’s also important to avoid relying on any specific behaviors not guaranteed by the specification. When, how, and whether garbage collection occurs is down to the implementation of any given JavaScript engine.

That said, achieving your goal is totally possible; your test case is just too simple (in light of the notes quoted above) to show it. Here’s a fixed version:

const lookup = new Map();

(function () {
  let element = { id: "someid", data: {} };
  lookup.set(element.id, new WeakRef(element));
  element = null;

  console.log(lookup.get("someid").deref());

  setTimeout(() => {
    global.gc();
    console.log(lookup.get("someid").deref());
  }, 0);
})();