Forgotten timers
setInterval registers a repeating handle — a GC root — until clearInterval runs. The callback’s closure captures every variable it references.
sequenceDiagram participant Timer as setInterval handle participant CB as Callback closure participant Data as Captured arrays Timer->>CB: invoke every N ms CB->>Data: push more data Note over Data: Grows forever if never cleared
Broken code
Section titled “Broken code”function startPolling(fetchUser) { setInterval(async () => { const profile = await fetchUser(); // large object history.push(profile); // outer scope array grows }, 5000); // interval id lost — cannot clear}Fixed code
Section titled “Fixed code”let pollId = null;const history = [];const MAX = 50;
function startPolling(fetchUser) { pollId = setInterval(async () => { const profile = await fetchUser(); history.push(profile); if (history.length > MAX) history.shift(); }, 5000);}
function stopPolling() { if (pollId !== null) { clearInterval(pollId); pollId = null; }}In frameworks, return cleanup from useEffect (React) or onDestroy (Svelte).
Live demo
Section titled “Live demo”Live demo: forgotten setInterval
Idle — each interval tick retains a growing closure scope.
`performance.memory` is only available in Chromium-based browsers. Use Chrome for live heap readouts.