Skip to content
  • Garbage Collection & Memory Leaks · JavaScript

Garbage Collection & Memory Leaks

LearnViaNotes @learnvianotes· 3 min read

JavaScript frees memory automatically once objects become unreachable. Leaks happen when you accidentally keep them reachable.

Reachability

The collector starts from roots — globals, the current call stack, active closures, pending callbacks — and marks everything reachable from them. Anything not marked is garbage, even if it points to other garbage (so cycles are fine).

  1. Mark

    Traverse from the roots and mark every reachable object.

  2. Sweep / compact

    Reclaim unmarked memory; optionally move live objects together.

Common leaks

LeakWhy it stays reachableFix
Forgotten timerssetInterval callback (and everything it closes over) is referenced by the timerclearInterval on cleanup
Event listenersListener closures on long-lived targets (window, document, emitters)removeEventListener, { signal }, { once: true }
Unbounded caches / MapsKeys and values are held strongly foreverLRU limit, WeakMap, TTL
Closures capturing big dataA small long-lived function keeps a huge outer variable aliveCopy only what's needed; null out references
Detached DOM nodesRemoved from the page but still referenced from JSDrop references; use WeakMap for per-node data
Accidental globalsSloppy-mode x = … creates a global rootStrict mode / modules
JavaScript
import { EventEmitter } from "node:events";
const bus = new EventEmitter();

function subscribeLeaky() {
  const bigBuffer = new Array(1e5).fill("*");      // captured by the listener below
  bus.on("tick", () => bigBuffer.length);
}
for (let i = 0; i < 3; i++) subscribeLeaky();
console.log("listeners after 3 leaky subscribes:", bus.listenerCount("tick"));

function subscribeClean(signal) {
  const handler = () => {};
  bus.on("tock", handler);
  signal.addEventListener("abort", () => bus.off("tock", handler));
}
const ac = new AbortController();
subscribeClean(ac.signal);
ac.abort();                                       // component unmounted
console.log("listeners after cleanup:", bus.listenerCount("tock"));
Output
listeners after 3 leaky subscribes: 3
listeners after cleanup: 0

Measuring

JavaScript
const before = process.memoryUsage().heapUsed;
let data = Array.from({ length: 200_000 }, (_, i) => ({ i }));
const during = process.memoryUsage().heapUsed;
console.log("grew by more than 5 MB:", during - before > 5 * 1024 * 1024);
data = null;   // now unreachable — eligible for collection
Output
grew by more than 5 MB: true
  • Browser: DevTools → Memory → heap snapshots (compare two) and the allocation timeline; look for growing Detached elements.

  • Node: process.memoryUsage(), node --inspect + Chrome DevTools, --heapsnapshot-signal, v8.writeHeapSnapshot().

How does garbage collection work in JavaScript?

Can circular references cause memory leaks?

Free for students and teachers

Write notes like this one

Cards for every idea, quizzes and flashcards, read aloud — on the web and your phone.

Something wrong with this note? Open it to report