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?
from How can I prevent a DOM node removed from its tree from being held by spurious strong references, like from closures?
No comments:
Post a Comment