DEV Community

Cover image for JavaScript Memory Leaks Usually Start With One Reference
Mark Yu
Mark Yu

Posted on • Edited on

JavaScript Memory Leaks Usually Start With One Reference

Most JavaScript memory leaks I have debugged were not dramatic.

They were one forgotten event listener, one timer that never stopped, or one closure holding a reference longer than expected.

The browser cannot collect memory if your code still points at it.

That is the whole game.

Stack vs Heap

The simplified model:

stack -> function calls, local primitives, references
heap  -> objects, arrays, functions, DOM nodes
Enter fullscreen mode Exit fullscreen mode

Example:

function createUser() {
  const name = "Mark";
  const user = { name, role: "admin" };
  return user;
}
Enter fullscreen mode Exit fullscreen mode

name is simple. user is an object on the heap, and the variable holds a reference to it.

When no live reference can reach that object, garbage collection can reclaim it.

The Leak Pattern

const cache = [];

function rememberUser(user) {
  cache.push(user);
}
Enter fullscreen mode Exit fullscreen mode

If cache grows forever, those users stay reachable forever.

Visual map:

global cache -> user object -> nested profile -> big data
Enter fullscreen mode Exit fullscreen mode

The garbage collector is not failing. Your references are doing exactly what you asked.

Event Listener Leak

This is common in UI code:

function mount() {
  window.addEventListener("resize", handleResize);
}
Enter fullscreen mode Exit fullscreen mode

If you mount/unmount repeatedly and never remove the listener, old handlers stay alive.

Better:

function mount() {
  window.addEventListener("resize", handleResize);

  return () => {
    window.removeEventListener("resize", handleResize);
  };
}
Enter fullscreen mode Exit fullscreen mode

React version:

useEffect(() => {
  window.addEventListener("resize", handleResize);
  return () => window.removeEventListener("resize", handleResize);
}, []);
Enter fullscreen mode Exit fullscreen mode

Timer Leak

setInterval(() => {
  refreshDashboard();
}, 5000);
Enter fullscreen mode Exit fullscreen mode

If the page or component is gone but the interval is still running, you have a leak or at least wasted work.

const intervalId = setInterval(refreshDashboard, 5000);
clearInterval(intervalId);
Enter fullscreen mode Exit fullscreen mode

In React:

useEffect(() => {
  const id = setInterval(refreshDashboard, 5000);
  return () => clearInterval(id);
}, []);
Enter fullscreen mode Exit fullscreen mode

Closure Surprise

Closures are useful, but they can keep data alive.

function createHandler(bigPayload) {
  return function onClick() {
    console.log(bigPayload.id);
  };
}
Enter fullscreen mode Exit fullscreen mode

If onClick is stored somewhere long-lived, bigPayload stays alive too.

That is not a bug in closures. It is how closures work.

Debugging Checklist

In Chrome DevTools:

  1. Open Memory panel.
  2. Take a heap snapshot.
  3. Interact with the page.
  4. Force garbage collection if appropriate.
  5. Take another snapshot.
  6. Compare retained objects.

Look for:

  • detached DOM nodes
  • growing arrays/maps
  • duplicate listeners
  • long-lived closures
  • cached API responses with no eviction

Why This Still Matters in 2026

Modern frontends are heavier now: dashboards, AI chat UIs, long-context document tools, canvas apps, and multimodal assistants can all keep large objects around.

If your app stores every token, message, image preview, and intermediate state forever, the browser will eventually punish you.

Final Thought

JavaScript memory management is automatic, not magical.

The garbage collector cleans unreachable objects. Your job is to stop accidentally keeping everything reachable.

What was the weirdest memory leak you have debugged in JavaScript?

Top comments (0)