| |

JavaScript 65 🧬 Memory management / garbage collection

JavaScript manages memory for you. You create objects, arrays, and closures — and the engine reclaims them when they’re no longer needed. That’s the deal. In exchange, you can’t manually free memory, and you can’t force garbage collection. But you can create leaks, and you can prevent them.

Understanding how garbage collection works isn’t about writing faster code. It’s about not writing code that leaks — the timers you forget to clear, the listeners you don’t remove, the caches that grow forever, the closures that hold references longer than they should.

Key point: The garbage collector reclaims memory for objects that are unreachable from the root (the global object, the call stack, and any active closures). If anything reachable still points to an object, it stays alive. Leaks are almost always an accidentally reachable reference.


a – How garbage collection works

JavaScript uses tracing garbage collection. The collector starts from a set of roots — global variables, the current call stack, active closures — and follows every reference. Anything reachable stays. Anything not reachable is collected.

The mark-and-sweep algorithm:

  1. Mark — start from roots, traverse every reachable reference, mark each object
  2. Sweep — free every unmarked object
  3. Compact — optionally move objects to reduce fragmentation

This runs periodically, not on every allocation. The exact timing depends on the engine, memory pressure, and heuristics.

What counts as a root:

  • Global variables (window, globalThis)
  • The current call stack (local variables, parameters)
  • Active closures (captured variables)
  • DOM references (in the browser)
  • Timers and pending callbacks

Reachability, not reference counting:

Modern engines don’t use reference counting. This matters because it handles circular references correctly:

function makeCycle() {
  const a = {};
  const b = {};
  a.b = b;
  b.a = a;
  return 'done';
}

makeCycle();
// a and b are unreachable now — collected even though they reference each other

Reference counting would keep them alive forever (each has a count of 1). Mark-and-sweep collects them because neither is reachable from a root.

Generational collection:

Most engines split memory into two generations:

  • Young generation (nursery) — newly created objects. Collected frequently and cheaply.
  • Old generation — objects that survived a few collections. Collected rarely but more expensively.

This is why allocating many short-lived objects is cheap — they die young and are collected fast.

The triggers:

Garbage collection runs when:

  • The young generation fills up
  • The old generation hits a threshold
  • The engine is idle
  • Memory pressure signals arrive from the OS

You can’t trigger it from JavaScript. window.gc() exists only in Chrome with a flag, and even then it’s a hint.

Why this matters for your code:

You don’t control GC timing. You control reachability. The only way to free memory is to make objects unreachable:

  • Remove references from variables
  • Delete from arrays, Maps, Sets
  • Remove event listeners
  • Clear timers
  • Close WeakRef targets (implicitly)

The delete operator:

const obj = { a: 1, b: 2 };
delete obj.a;   // removes the property, allows collection of the value

delete removes the property, not the object. If nothing else references the value, it becomes collectable.

Setting to null:

let big = new Array(1e7).fill(0);
big = null;   // makes the array unreachable

Assigning null (or any other value) drops the reference. The array is now collectable.

Scope and reachability:

function process() {
  const huge = new Array(1e7).fill(0);
  doWork(huge);
  // huge goes out of scope at the end of process
}

Once process() returns, huge is unreachable — unless doWork stored a reference somewhere.

Closures keep things alive:

function outer() {
  const data = new Array(1e6).fill(0);
  return function inner() {
    return data.length;
  };
}

const fn = outer();
// `data` stays alive as long as `fn` is reachable

The closure keeps data reachable even though outer has returned. This is intentional — but it’s also how many leaks start.

Common roots that surprise you:

RootWhy it keeps things alive
Global variablesNever go out of scope
TimersThe callback may reference anything
Event listenersThe listener closure holds scope
ClosuresCaptured variables stay reachable
DOM nodesThe DOM is reachable from the document
PromisesPending reactions hold references

Memory isn’t unlimited:

Each browser tab has a heap limit — typically around 2GB on desktop, less on mobile. Exceeding it throws RangeError: Maximum call stack size exceeded or causes the tab to crash. Node.js defaults to roughly 1.5GB, adjustable with --max-old-space-size.


b – What causes memory leaks

A memory leak is memory that’s still reachable but no longer useful. The GC can’t collect it because something still points to it — even if that something is a forgotten reference.

Leak 1 — Global variables:

function process() {
  userData = new Array(1e7);  // ❌ missing let/const
}

An undeclared assignment creates a global. It stays alive for the lifetime of the page. This is the classic accidental leak — and why 'use strict' helps.

Leak 2 — Forgotten timers:

setInterval(() => {
  const data = hugeArray.slice();
  render(data);
}, 1000);

The interval callback captures hugeArray and everything else in scope. As long as the interval runs, nothing it references can be collected.

const interval = setInterval(tick, 1000);

// Later
clearInterval(interval);   // ✅

Leak 3 — Unremoved event listeners:

element.addEventListener('click', () => {
  // captures outer variables
  doSomething();
});

The listener stays attached to the element. If the element stays in the DOM, the listener and everything it captures stay alive. Removing the element does not remove the listener automatically in all cases.

function handler() { ... }
element.addEventListener('click', handler);

// Later
element.removeEventListener('click', handler);   // ✅

Or use AbortController:

const controller = new AbortController();
element.addEventListener('click', handler, { signal: controller.signal });

// Later
controller.abort();   // ✅ removes all listeners

Leak 4 — Detached DOM nodes:

const nodes = [];

function addNode() {
  const div = document.createElement('div');
  document.body.appendChild(div);
  nodes.push(div);
}

function removeNode() {
  const div = nodes.pop();
  document.body.removeChild(div);
  // ❌ the div is still referenced by anything that captured it
}

Removing a node from the DOM doesn’t free it if JavaScript still references it. Common culprits: Map keyed by DOM nodes, arrays of elements, listener closures that capture the node.

Leak 5 — Closures holding large data:

function setup() {
  const bigData = new Array(1e7).fill(0);

  document.querySelector('#btn').addEventListener('click', () => {
    console.log('clicked');   // ❌ doesn't use bigData, but captures the scope
  });
}

The listener doesn’t use bigData, but it’s in the same scope. Depending on the engine, it may still keep bigData alive. Isolate large data or remove the listener when done.

Leak 6 — Caches that grow forever:

const cache = new Map();

function get(key) {
  if (!cache.has(key)) {
    cache.set(key, compute(key));
  }
  return cache.get(key);
}

The cache never evicts. For long-running apps, this grows without bound. Solutions:

  • LRU cache with a max size
  • WeakMap for object keys — auto-cleans when the key dies
  • TTL cache with expiration
  • WeakRef-based cache that doesn’t prevent collection

Leak 7 — Promises that never resolve:

function waitForever() {
  return new Promise(() => {
    // never resolves, never rejects
  });
}

waitForever().then(() => console.log('never'));

The Promise stays pending. The .then reaction holds a reference. If this happens many times, they accumulate.

Leak 8 — Unbounded arrays:

const events = [];

function log(event) {
  events.push(event);   // grows forever
}

A log that never rotates or caps is a leak by design.

Leak 9 — Subscriptions not cleaned:

class Component {
  constructor(store) {
    store.subscribe(() => this.update());   // ❌ never unsubscribes
  }
  update() { ... }
}

When the component is destroyed, the store still calls update() — and everything the closure captured stays alive.

Leak 10 — Detached iframes and workers:

const iframe = document.createElement('iframe');
document.body.appendChild(iframe);

// Later
document.body.removeChild(iframe);
// ❌ iframe may keep running until the reference is dropped
iframe = null;

Iframes and Workers keep running until they’re torn down. Always terminate workers and clean up iframe references.

Symptoms of leaks:

  • Memory grows steadily in DevTools
  • Page gets slower over time
  • Tab crashes after long use
  • GC runs more often (thrashing)
  • performance.memory (Chrome) shows a rising heap

How to find leaks:

Chrome DevTools → Memory tab:

  1. Take a heap snapshot
  2. Interact with the app
  3. Take another snapshot
  4. Compare — objects that shouldn’t still exist are the leak

Or use Allocation instrumentation on timeline to watch allocations in real time.

The general rule:

If something is reachable but no longer useful, it’s a leak. Find the reference that keeps it reachable and remove it.


c – Preventing leaks

The best defense is discipline. Clean up what you create, size your caches, and avoid unintentional references.

Rule 1 — Clean up timers and intervals:

class Poller {
  #interval;

  start() {
    this.#interval = setInterval(() => this.poll(), 1000);
  }

  stop() {
    clearInterval(this.#interval);
  }
}

Any setInterval needs a corresponding clearInterval.

Rule 2 — Remove listeners:

class Widget {
  #controller = new AbortController();

  constructor(el) {
    el.addEventListener('click', this.handleClick, {
      signal: this.#controller.signal
    });
  }

  destroy() {
    this.#controller.abort();
  }

  handleClick = () => { ... };
}

AbortController makes cleanup a one-liner.

Rule 3 — Use WeakMap for object-keyed data:

const metadata = new WeakMap();

function attachMeta(obj, meta) {
  metadata.set(obj, meta);
}

When obj is collected, its metadata goes with it. No cleanup needed.

Rule 4 — Size your caches:

class LRUCache {
  #max;
  #map = new Map();

  constructor(max = 100) {
    this.#max = max;
  }

  get(key) {
    if (!this.#map.has(key)) return undefined;
    const value = this.#map.get(key);
    this.#map.delete(key);
    this.#map.set(key, value);
    return value;
  }

  set(key, value) {
    if (this.#map.has(key)) this.#map.delete(key);
    this.#map.set(key, value);

    if (this.#map.size > this.#max) {
      const oldest = this.#map.keys().next().value;
      this.#map.delete(oldest);
    }
  }
}

A bounded cache can’t grow forever.

Rule 5 — Avoid accidental globals:

'use strict';
// or use ES modules (always strict)

function process() {
  const data = [];  // ✅ properly scoped
}

'use strict' or ES modules make accidental globals a ReferenceError.

Rule 6 — Null out large references:

function process() {
  let data = loadHugeData();
  doWork(data);

  data = null;   // ✅ free early if the function continues
  doOtherWork();
}

If a function continues after using large data, drop the reference.

Rule 7 — Clean up subscriptions:

class View {
  #unsubscribe;

  constructor(store) {
    this.#unsubscribe = store.subscribe(() => this.render());
  }

  destroy() {
    this.#unsubscribe();
  }
}

Return an unsubscribe function from subscribe, call it on destroy.

Rule 8 — Use WeakRef carefully:

const cache = new Map();

function set(key, value) {
  cache.set(key, new WeakRef(value));
}

The cache won’t prevent collection. But you have to check .deref() and handle undefined.

Rule 9 — Detach DOM nodes fully:

function removeElement(el) {
  el.remove();
  el.replaceWith(null);  // or clear children
  // Drop external references
}

Or use MutationObserver / WeakMap for tracking DOM metadata.

Rule 10 — Terminate workers and iframes:

const worker = new Worker('task.js');

// Later
worker.terminate();
worker = null;

Workers keep running until terminated. Iframes need their src cleared or the node properly detached.

Rule 11 — Reuse objects when appropriate:

// Instead of allocating per frame:
let temp = { x: 0, y: 0 };

function update(x, y) {
  temp.x = x;
  temp.y = y;
  render(temp);
}

Not required for correctness, but reduces GC pressure in hot loops.

Rule 12 — Prefer WeakMap/WeakSet for tagging:

const processed = new WeakSet();

function mark(obj) {
  processed.add(obj);
}

Metadata attached via WeakMap/WeakSet disappears when the object does.

Checklist before shipping:

  • Every setInterval has a clearInterval
  • Every addEventListener has a matching removal (or uses AbortController)
  • Every subscription has an unsubscribe
  • Caches have a max size or use WeakMap
  • No accidental globals
  • No large captured variables in long-lived closures
  • Workers and iframes are terminated
  • Detached DOM nodes aren’t held by arrays or Maps

What the browser does for you:

  • Collects unreachable objects automatically
  • Handles cycles correctly
  • Uses generational GC for speed
  • Compacts memory to reduce fragmentation

What it doesn’t do:

  • Run on demand
  • Clean up reachable-but-unused references
  • Detect semantic leaks (caches that grow forever)
  • Warn you about forgotten listeners

Tools to detect leaks:

ToolPurpose
Chrome DevTools → MemoryHeap snapshots, allocation timeline
performance.memory (Chrome)Current heap usage
Node --inspectSame tools for Node
process.memoryUsage()Node heap stats
FinalizationRegistryDetect when objects are collected
WeakRefObserve collection indirectly

A quick leak test:

function checkMemory(label) {
  if (performance.memory) {
    const mb = performance.memory.usedJSHeapSize / 1024 / 1024;
    console.log(`${label}: ${mb.toFixed(2)} MB`);
  }
}

checkMemory('before');
doLotsOfWork();
checkMemory('after');

If memory keeps climbing after work completes, something is still referenced.


Complete Example Session

// ============================================
// PART 1: REACHABILITY
// ============================================

let obj = { data: new Array(1e6).fill(0) };

obj = null;
// obj is now collectable
// GC will free it when it runs

// ============================================
// PART 2: CLOSURES HOLD REFERENCES
// ============================================

function outer() {
  const data = new Array(1e6).fill(0);
  return function inner() {
    return data.length;
  };
}

const fn = outer();
// `data` stays alive as long as `fn` is reachable

// ============================================
// PART 3: CIRCULAR REFERENCES
// ============================================

function makeCycle() {
  const a = {};
  const b = { a };
  a.b = b;
  return 'done';
}

makeCycle();
// Both a and b are collectable despite the cycle

// ============================================
// PART 4: ACCIDENTAL GLOBAL
// ============================================

function leak() {
  leakedData = new Array(1e7);  // ❌ global
}

// In strict mode or modules:
try {
  leak();
} catch (err) {
  console.log(err.message);
  // [ leakedData is not defined ]
}

// ============================================
// PART 5: TIMER LEAK
// ============================================

const huge = new Array(1e7).fill(0);

const interval = setInterval(() => {
  console.log(huge.length);   // captures `huge`
}, 1000);

// Later
clearInterval(interval);
// Now `huge` can be collected (if nothing else references it)

// ============================================
// PART 6: LISTENER LEAK
// ============================================

const el = document.querySelector('#btn');

function onClick() {
  console.log('clicked');
}

el.addEventListener('click', onClick);

// Later
el.removeEventListener('click', onClick);
// Now the closure can be collected

// ============================================
// PART 7: ABORT CONTROLLER
// ============================================

const controller = new AbortController();

el.addEventListener('click', onClick, { signal: controller.signal });

// Later
controller.abort();
// All listeners using this signal are removed

// ============================================
// PART 8: WEAKMAP FOR METADATA
// ============================================

const meta = new WeakMap();

function tag(obj, info) {
  meta.set(obj, info);
}

let user = { name: 'Alice' };
tag(user, { created: Date.now() });
user = null;
// The metadata entry is collectable with `user`

// ============================================
// PART 9: UNBOUNDED CACHE
// ============================================

const cache = new Map();

function get(key) {
  if (!cache.has(key)) {
    cache.set(key, compute(key));
  }
  return cache.get(key);
}

// ❌ grows forever — needs eviction

// ============================================
// PART 10: LRU CACHE
// ============================================

class LRU {
  #max;
  #map = new Map();

  constructor(max = 100) {
    this.#max = max;
  }

  get(key) {
    if (!this.#map.has(key)) return undefined;
    const v = this.#map.get(key);
    this.#map.delete(key);
    this.#map.set(key, v);
    return v;
  }

  set(key, value) {
    if (this.#map.has(key)) this.#map.delete(key);
    this.#map.set(key, value);

    if (this.#map.size > this.#max) {
      const oldest = this.#map.keys().next().value;
      this.#map.delete(oldest);
    }
  }
}

const lru = new LRU(3);
lru.set('a', 1);
lru.set('b', 2);
lru.set('c', 3);
lru.set('d', 4);
// 'a' evicted

// ============================================
// PART 11: DETACHED DOM
// ============================================

const nodeRefs = [];

function addBox() {
  const box = document.createElement('div');
  document.body.appendChild(box);
  nodeRefs.push(box);
}

function removeLast() {
  const box = nodeRefs.pop();
  if (box) box.remove();
}
// Node is freed when popped from the array

// ============================================
// PART 12: TERMINATING WORKERS
// ============================================

const worker = new Worker('task.js');

// Later
worker.terminate();
// Worker stops and is collectable

// ============================================
// PART 13: DETECTING LEAKS
// ============================================

function checkMemory(label) {
  if (performance.memory) {
    const mb = performance.memory.usedJSHeapSize / 1024 / 1024;
    console.log(`${label}: ${mb.toFixed(2)} MB`);
  }
}

// checkMemory('before');
// heavyWork();
// checkMemory('after');

// ============================================
// PART 14: FINALIZATION REGISTRY
// ============================================

const registry = new FinalizationRegistry((id) => {
  console.log('Collected:', id);
});

let tracked = { name: 'temp' };
registry.register(tracked, 'obj-1');
tracked = null;
// Eventually: [ Collected: obj-1 ]

// ============================================
// PART 15: CLEANUP CLASS
// ============================================

class Resource {
  #controller = new AbortController();
  #interval;

  constructor(el) {
    el.addEventListener('click', this.handle, {
      signal: this.#controller.signal
    });

    this.#interval = setInterval(this.tick, 1000);
  }

  handle = () => {};
  tick = () => {};

  destroy() {
    this.#controller.abort();
    clearInterval(this.#interval);
  }
}

// ============================================
// PART 16: TESTING FOR LEAKS
// ============================================

class Tracked {
  static alive = 0;
  static registry = new FinalizationRegistry(() => {
    Tracked.alive--;
  });

  constructor() {
    Tracked.alive++;
    Tracked.registry.register(this, this);
  }
}

function createMany() {
  const arr = [];
  for (let i = 0; i < 1000; i++) arr.push(new Tracked());
  return arr.length;
}

createMany();
console.log(Tracked.alive);
// Eventually drops back to 0 when GC runs

Quick Reference

Reachability

ConceptMeaning
RootGlobal, stack, closure
ReachableHas a path from a root
CollectableUnreachable
LeakReachable but unused

GC Algorithm

PhaseAction
MarkTraverse from roots
SweepFree unmarked
CompactDefragment

Generations

GenerationContainsCollected
YoungNew objectsFrequently
OldSurvivorsRarely

Common Leaks

SourceFix
Global variablesUse let/const
setIntervalclearInterval
ListenersremoveEventListener / AbortController
ClosuresIsolate or remove
CachesBound size / WeakMap
Detached DOMDrop references
Pending PromisesResolve or reject
Workers/iframesTerminate

Cleanup API

APIPurpose
clearTimeoutCancel timeout
clearIntervalCancel interval
removeEventListenerRemove listener
AbortControllerRemove listeners in bulk
WeakMapAuto-collect with key
WeakRefDon’t prevent GC
FinalizationRegistryDetect collection

Detecting Leaks

ToolPlatform
DevTools MemoryBrowser
performance.memoryChrome
process.memoryUsage()Node
Heap snapshotsChrome/Node
Allocation timelineChrome

Do’s and Don’ts

DoDon’t
Clear timersLeak intervals
Remove listenersIgnore cleanup
Bound cachesGrow forever
Use WeakMapUse Map for object keys
Terminate workersLeave running
Scope variablesImplicit globals

Best Practices

Do This:

// Clear every timer
const id = setInterval(tick, 1000);
clearInterval(id);                              // ✅

// Use AbortController for listeners
const c = new AbortController();
el.addEventListener('click', fn, { signal: c.signal });
c.abort();                                      // ✅

// Bound your caches
class LRU { ... }                               // ✅

// Use WeakMap for metadata
const meta = new WeakMap();                     // ✅

// Drop references when done
data = null;                                    // ✅

// Terminate workers
worker.terminate();                             // ✅

// Scope variables properly
function f() {
  const data = [];                              // ✅
}

// Return unsubscribe functions
const off = store.subscribe(fn);
off();                                          // ✅

Don’t Do This:

// Don't forget to clear intervals
setInterval(tick, 1000);                        // ❌ runs forever

// Don't leave listeners attached
el.addEventListener('click', fn);               // ⚠️  no removal

// Don't create implicit globals
data = [];                                      // ❌ global

// Don't cache without bounds
cache.set(key, hugeValue);                      // ❌ grows forever

// Don't capture huge data in closures
bigData; el.addEventListener('click', () => {}); // ⚠️  holds scope

// Don't leave workers running
new Worker('task.js');                          // ❌ never terminated

// Don't hold detached DOM
detachedNodes.push(node);                       // ⚠️  leaks

// Don't rely on gc() to fix it
window.gc();                                    // ⚠️  not standard

Common Pitfalls

PitfallProblemSolution
setInterval without clearRuns foreverclearInterval
Listeners without removalClosures stay aliveRemove or abort
Implicit globalsNever collected'use strict'
Unbounded cacheGrows until crashLRU / TTL
Closures capturing big dataRetainedIsolate or null out
Detached DOM nodesHeld by referencesDrop them
Pending PromisesHeld foreverResolve/reject
Workers not terminatedKeep runningterminate()
Circular referencesActually fineGC handles them
Assuming GC runsNon-deterministicNever rely on timing

Real-World Examples

1. Clearing an interval

const id = setInterval(() => console.log('tick'), 1000);
clearInterval(id);

The only correct way to stop a repeating timer.

2. Removing a listener

function onClick() {}
el.addEventListener('click', onClick);
el.removeEventListener('click', onClick);

Same reference on both sides — the listener is freed.

3. Aborting many listeners

const c = new AbortController();
el.addEventListener('click', fn1, { signal: c.signal });
el.addEventListener('keydown', fn2, { signal: c.signal });
c.abort();

One call removes both.

4. WeakMap for metadata

const meta = new WeakMap();
meta.set(el, { created: Date.now() });
// When `el` is removed and collectable, the metadata goes with it

No explicit cleanup needed.

5. LRU cache

class LRU {
  #max;
  #map = new Map();
  constructor(max = 100) { this.#max = max; }
  set(k, v) {
    if (this.#map.has(k)) this.#map.delete(k);
    this.#map.set(k, v);
    if (this.#map.size > this.#max) {
      this.#map.delete(this.#map.keys().next().value);
    }
  }
}

Bounded by design.

6. Detaching DOM

function removeBox(box) {
  box.remove();
  box = null;  // drop the reference
}

Removing from the DOM and dropping references — both required.

7. Terminating workers

const worker = new Worker('task.js');
worker.terminate();

Stops the worker thread immediately.

8. Cleanup class

class Cleanup {
  #c = new AbortController();
  #id;
  constructor() {
    document.addEventListener('click', this.onClick, { signal: this.#c.signal });
    this.#id = setInterval(this.tick, 1000);
  }
  onClick = () => {};
  tick = () => {};
  destroy() {
    this.#c.abort();
    clearInterval(this.#id);
  }
}

Every subscription captured, one method to release them all.

9. Memory check

if (performance.memory) {
  console.log(performance.memory.usedJSHeapSize);
}

Quick check — Chrome-only, but useful for local debugging.

10. Leak test

class Tracked {
  static alive = 0;
  static reg = new FinalizationRegistry(() => Tracked.alive--);
  constructor() {
    Tracked.alive++;
    Tracked.reg.register(this, this);
  }
}

for (let i = 0; i < 1000; i++) new Tracked();
// alive drops back when GC runs — if it doesn't, something holds them

Visual: Reachability

┌──────────────────────────────────────────────┐
│  Roots                                       │
│                                              │
│  window ──► A ──► B                          │
│                    │                         │
│                    ▼                         │
│                    C ──► D                   │
│                                              │
│  All reachable: A, B, C, D                   │
│                                              │
│  If A = null:                                │
│  B, C, D unreachable ──► collected           │
│                                              │
└──────────────────────────────────────────────┘

Visual: Mark and Sweep

┌──────────────────────────────────────────────┐
│  Step 1: Mark                                │
│                                              │
│  From roots, follow every reference          │
│  Mark each object reached                    │
│                                              │
│  Step 2: Sweep                               │
│                                              │
│  Free every unmarked object                  │
│                                              │
│  Step 3: Compact                             │
│                                              │
│  Move surviving objects together             │
│  Reduces fragmentation                       │
│                                              │
└──────────────────────────────────────────────┘

Visual: Circular References Are Fine

┌──────────────────────────────────────────────┐
│  a ──► b                                     │
│  ▲     │                                     │
│  └─────┘                                     │
│                                              │
│  Reachable from root? No                     │
│                                              │
│  Reference count: 1 each (never reaches 0)   │
│                                              │
│  Mark-and-sweep: not reachable → collected   │
│                                              │
│  Modern GC handles this correctly            │
│                                              │
└──────────────────────────────────────────────┘

Visual: Leak Sources

┌──────────────────────────────────────────────┐
│  Common leak sources                         │
│                                              │
│  📌 Global variables                         │
│  ⏰ Intervals / timeouts                     │
│  🎧 Event listeners                          │
│  🔒 Closures capturing data                  │
│  💾 Unbounded caches                         │
│  🎨 Detached DOM nodes                       │
│  ⚙️  Workers / iframes                       │
│  🌐 Pending Promises                         │
│                                              │
└──────────────────────────────────────────────┘

Summary

ConceptMeaning
GCAutomatic memory reclaim
ReachableHas path from root
CollectableNo path from root
LeakReachable but unused
Mark-sweepThe algorithm
GenerationYoung vs old
RootGlobal, stack, closure
WeakMapAuto-collect with key
AbortControllerBulk listener removal
LRUBounded cache

Key takeaways:

  • Garbage collection is automatic — you can’t trigger it, only make objects unreachable
  • Reachability is what matters — anything reachable from a root stays alive
  • Mark-and-sweep handles circular references correctly — modern engines don’t use reference counting
  • Generational GC collects young objects cheaply and often, old objects rarely
  • Leaks are reachable-but-unused references — find what still points and remove it
  • Global variables, timers, listeners, closures, caches, and detached DOM are the top leak sources
  • Clear every timerclearInterval, clearTimeout
  • Remove every listenerremoveEventListener or AbortController
  • Bound your caches — LRU, TTL, or WeakMap
  • Use WeakMap and WeakSet for object-keyed data that should auto-collect
  • Null out large references when you’re done with them
  • Terminate workers and iframes — they keep running until you stop them
  • Detect leaks with DevTools heap snapshots, FinalizationRegistry, or performance.memory
  • Never rely on GC timing — non-deterministic across engines and situations

Remember: JavaScript frees memory for you — but only if you make it possible. The GC can’t collect what’s still reachable, and long-lived references are the source of nearly every leak. Clean up your timers, remove your listeners, bound your caches, and drop large references. When memory grows unexpectedly, take a heap snapshot and find what’s still pointing. Master reachability, and your apps run for hours without leaking.


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!