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:
- Mark — start from roots, traverse every reachable reference, mark each object
- Sweep — free every unmarked object
- 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
WeakReftargets (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:
| Root | Why it keeps things alive |
|---|---|
| Global variables | Never go out of scope |
| Timers | The callback may reference anything |
| Event listeners | The listener closure holds scope |
| Closures | Captured variables stay reachable |
| DOM nodes | The DOM is reachable from the document |
| Promises | Pending 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:
- Take a heap snapshot
- Interact with the app
- Take another snapshot
- 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
setIntervalhas aclearInterval - Every
addEventListenerhas a matching removal (or usesAbortController) - 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:
| Tool | Purpose |
|---|---|
| Chrome DevTools → Memory | Heap snapshots, allocation timeline |
performance.memory (Chrome) | Current heap usage |
Node --inspect | Same tools for Node |
process.memoryUsage() | Node heap stats |
FinalizationRegistry | Detect when objects are collected |
WeakRef | Observe 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
| Concept | Meaning |
|---|---|
| Root | Global, stack, closure |
| Reachable | Has a path from a root |
| Collectable | Unreachable |
| Leak | Reachable but unused |
GC Algorithm
| Phase | Action |
|---|---|
| Mark | Traverse from roots |
| Sweep | Free unmarked |
| Compact | Defragment |
Generations
| Generation | Contains | Collected |
|---|---|---|
| Young | New objects | Frequently |
| Old | Survivors | Rarely |
Common Leaks
| Source | Fix |
|---|---|
| Global variables | Use let/const |
setInterval | clearInterval |
| Listeners | removeEventListener / AbortController |
| Closures | Isolate or remove |
| Caches | Bound size / WeakMap |
| Detached DOM | Drop references |
| Pending Promises | Resolve or reject |
| Workers/iframes | Terminate |
Cleanup API
| API | Purpose |
|---|---|
clearTimeout | Cancel timeout |
clearInterval | Cancel interval |
removeEventListener | Remove listener |
AbortController | Remove listeners in bulk |
WeakMap | Auto-collect with key |
WeakRef | Don’t prevent GC |
FinalizationRegistry | Detect collection |
Detecting Leaks
| Tool | Platform |
|---|---|
| DevTools Memory | Browser |
performance.memory | Chrome |
process.memoryUsage() | Node |
| Heap snapshots | Chrome/Node |
| Allocation timeline | Chrome |
Do’s and Don’ts
| Do | Don’t |
|---|---|
| Clear timers | Leak intervals |
| Remove listeners | Ignore cleanup |
| Bound caches | Grow forever |
Use WeakMap | Use Map for object keys |
| Terminate workers | Leave running |
| Scope variables | Implicit 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
| Pitfall | Problem | Solution |
|---|---|---|
setInterval without clear | Runs forever | clearInterval |
| Listeners without removal | Closures stay alive | Remove or abort |
| Implicit globals | Never collected | 'use strict' |
| Unbounded cache | Grows until crash | LRU / TTL |
| Closures capturing big data | Retained | Isolate or null out |
| Detached DOM nodes | Held by references | Drop them |
| Pending Promises | Held forever | Resolve/reject |
| Workers not terminated | Keep running | terminate() |
| Circular references | Actually fine | GC handles them |
| Assuming GC runs | Non-deterministic | Never 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
| Concept | Meaning |
|---|---|
| GC | Automatic memory reclaim |
| Reachable | Has path from root |
| Collectable | No path from root |
| Leak | Reachable but unused |
| Mark-sweep | The algorithm |
| Generation | Young vs old |
| Root | Global, stack, closure |
| WeakMap | Auto-collect with key |
| AbortController | Bulk listener removal |
| LRU | Bounded 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 timer —
clearInterval,clearTimeout - Remove every listener —
removeEventListenerorAbortController - Bound your caches — LRU, TTL, or
WeakMap - Use
WeakMapandWeakSetfor 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, orperformance.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!