For a toy example, suppose I have a clock widget:
{
const clockElem = document.getElementById('clock');
const timefmt = new Intl.DateTimeFormat(
'default', { timeStyle: 'medium', });
setInterval(() => {
const d = new Date;
console.log('tick', d);
clockElem.querySelector('p').innerHTML =
timefmt.format(d);
}, 1000);
clockElem.querySelector('button')
.addEventListener('click', ev => {
clockElem.remove();
});
}
<div id="clock">
<button>Remove</button>
<p></p>
</div>
When I click the button to remove the clock, the setInterval
callback is still invoked. The callback closure holds the DOM node strongly, which means its resources cannot be freed. There is also the circular reference from the button event handler; though perhaps that one could be handled by the engine’s cycle collector. Then again, maybe not.
Never fear: I can create a helper function ensuring that closures only hold the DOM node by a weak reference, and throw in FinalizationRegistry
to clean up the timer.
const weakCapture = (captures, func) => {
captures = captures.map(o => new WeakRef(o));
return (...args) => {
const objs = [];
for (const wr of captures) {
const o = wr.deref();
if (o === void 0)
return;
objs.push(o);
}
return func(objs, ...args);
}
};
const finregTimer = new FinalizationRegistry(
timerId => clearInterval(timerId));
{
const clockElem = document.getElementById('clock');
const timefmt = new Intl.DateTimeFormat(
'default', { timeStyle: 'medium', });
const timerId = setInterval(
weakCapture([clockElem], ([clockElem]) => {
const d = new Date;
console.log('tick', d);
clockElem.querySelector('p').innerHTML =
timefmt.format(d);
}), 1000);
finregTimer.register(clockElem, timerId);
clockElem.querySelector('button')
.addEventListener('click',
weakCapture([clockElem], ([clockElem], ev) => {
clockElem.remove();
}));
}
<div id="clock">
<button>Remove</button>
<p></p>
</div>
<button onclick="+'9'.repeat(1e9)">Try to force GC</button>
But this doesn’t seem to work. Even after the clockElem
node is removed, the ‘tick’ keeps being logged to the console, meaning the WeakRef
has not been emptied, meaning something seems to still hold a strong reference to clockElem
. Given that GC is not guaranteed to run immediately, I expected some delay, of course, but even when I try to force GC by running memory-heavy code like +'9'.repeat(1e9)
in the console, the weak reference is not cleared (despite this being enough to force GC and clear weak references in even more trivial cases like new WeakRef({})
). This happens both in Chromium (118.0.5993.117) and in Firefox (115.3.0esr).
Is this a flaw in the browsers? Or is there perhaps some other strong reference that I missed?
2
Answers
And, of course, if the code hadn’t been relying on d and timefmt to survive, it would have been possible to just pass those as the parameters to setInterval instead of relying on weakCapture:
And if there was other code that needed both the dom node and those values, we could pass a weak data object to setInterval and update which values it held each time they changed, again using weakCapture. The circular reference wouldn’t matter, even assuming a browser did not have an explicit cycle collector, since, after all, regular garbage collection can manage circular references.
Or perhaps, just set explicit callbacks instead of using setInterval.
On many pages of MDN there are remarks that you should not use GC’s events and functionality to handle an app’s LOGIC.
For example: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry
The WeakRef’s page says "Avoid where possible": https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakRef#avoid_where_possible
Instead I suggest to use some design pattern which I call
Object Tree
. So you keep all your objects in a hierarchy and you can destroy any tree node and all of its descendants in any time with freeing all resources.Actually this is implemented in any UI framework with a component tree. For example in Vue you have mounted/unmounted hooks to create/free resources and Vue destroys all descendant components automatically: