← Back to the library
FrontendDeep dive · 9 min

The app gets slower the longer the tab stays open

“A dashboard is fine after a reload but sluggish after a day open; the tab's memory in Chrome Task Manager climbs with every route change and never comes back down.”

THE PRINCIPLE

Whatever a component starts, its cleanup stops. A leaked reference from a long-lived object (window, a store, a cache, a socket) to short-lived UI keeps the whole subtree in memory.

FIRST MOVES

  1. Confirm it is a leak: navigate away and back 10 times, force garbage collection in the Memory panel, and watch whether JS heap and DOM node counts return to baseline.
  2. Take heap snapshots before and after the loop, compare them, and filter by "Detached" to find DOM trees removed from the page but still referenced.
  3. Follow the retainers of a detached node to the variable, listener, or closure holding it.
  4. Audit effects and lifecycle hooks for missing cleanup: addEventListener on window or document, setInterval, observers, socket and store subscriptions.
  5. Bound caches (module-level Maps, memo caches, query caches) with size limits or garbage-collection times, and abort in-flight requests on unmount.

TOOLS: THEN → NOW

Pairing each addEventListener with a removeEventListenerOne AbortController per effect; its signal removes listeners and cancels fetches together
Watching total memory and guessingHeap snapshot comparison with the Detached filter, and the Detached elements profile in DevTools

PATTERN SNAPSHOT

useEffect(() => {
  const controller = new AbortController();
  const { signal } = controller;

  window.addEventListener("resize", onResize, { signal });
  document.addEventListener("keydown", onKey, { signal });
  fetch(url, { signal }).then(r => r.json()).then(setData).catch(ignoreAbort);
  const id = setInterval(poll, 5_000);
  const unsubscribe = store.subscribe(onChange);

  return () => { controller.abort(); clearInterval(id); unsubscribe(); };
}, [url]);

CLOSE THE AI. EXPLAIN THIS.

A modal is closed and removed from the DOM, yet a heap snapshot still shows it as a detached tree. Name three kinds of references that could be keeping it alive.

HOW IT WORKS UNDERNEATH

SOURCES

Guide reviewed