What’s up everyone? I’m vaultboy and I’m trying to build a little pixel-art palette search UI where typing filters a big list and updates a preview, but after a few minutes the tab gets sluggish and memory in DevTools just keeps climbing.
const listeners = new Map();
export function wireSearch(input, store) {
const handler = () => {
clearTimeout(handler.t);
handler.t = setTimeout(() => {
store.setQuery(input.value); // triggers render
}, 80);
};
input.addEventListener('input', handler);
listeners.set(input, handler);
}
export function unwireSearch(input) {
const handler = listeners.get(input);
input.removeEventListener('input', handler);
// listeners.delete(input); // if I do this, sometimes I lose updates after remount
}
Is the Map holding DOM nodes and preventing GC (and if so what’s the right pattern here without introducing stale handlers or missed updates on remount)?
VaultBoy
yes — a plain Map can keep the input node alive, so the node, the handler closure, and whatever the closure captures can hang around longer than they should.
The timeout needs clearing on teardown too. If you only remove the event listener but leave handler.t pending, you can still get a late setQuery() after the component is gone, which is where the jank starts feeling haunted.
The remount issue sounds more like you’re unwiring the wrong element reference. Old input gets replaced, you call unwireSearch() on the new node, and the old listener is still sitting there on the dead DOM.
I’d keep the registry weak and clear the timer before removing the listener:
const listeners = new WeakMap();
export function wireSearch(input, store) {
if (listeners.has(input)) return;
const handler = () => {
clearTimeout(handler.t);
handler.t = setTimeout(() => store.setQuery(input.value), 80);
};
input.addEventListener('input', handler);
listeners.set(input, handler);
}
export function unwireSearch(input) {
const handler = listeners.get(input);
if (!handler) return;
clearTimeout(handler.t);
input.removeEventListener('input', handler);
listeners.delete(input);
}
If this is in a framework, I’d check the teardown path first. Half the time the “memory leak” is just a stale node reference and a timer that never got killed.