Memory Management in JavaScript
debt(d5/e5/b7/t5)
Closest to 'specialist tool catches it' (d5). Chrome DevTools Memory and clinic.js can detect heap growth, retained objects, and detached DOM nodes, but require manual inspection and profiling — not automatic linting. The detection_hints explicitly state 'automated: no', and the common patterns (unbounded Maps, event listeners without cleanup) are not caught by default linters.
Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix suggests adding AbortController or capping Maps, which may require changes across multiple event listener sites or refactoring cache initialization. For module-level globals and accumulated caches, fixes often span several locations within a component or service, not a single line.
Closest to 'strong gravitational pull' (b7). Memory management patterns in JavaScript shape how developers write event handlers, manage caches, initialize globals, and structure cleanup logic across the entire codebase. Poor choices early (unbounded module-scope Maps, listener accumulation) force future maintainers to be vigilant about GC implications in every feature addition, and the patterns are deeply embedded in how JavaScript execution and retention work.
Closest to 'notable trap' (t5). The misconception field directly states the trap: 'Setting a variable to null does not immediately free memory.' This is a documented gotcha that most JavaScript developers eventually learn through production incidents. Reference semantics, closure retention, and GC scheduling are non-obvious relative to languages with explicit memory management, but the behavior is well-documented and becomes expected with experience.
Also Known As
TL;DR
Explanation
JavaScript engines (V8, SpiderMonkey) use a mark-and-sweep garbage collector: starting from GC roots (global scope, call stack), the collector marks all reachable objects and sweeps the rest. Memory is allocated on the heap for objects, arrays, closures, and strings, and on the stack for primitives in function scope. Leaks occur when references are unintentionally kept alive — common sources: event listeners not removed when elements are detached from the DOM, closures capturing large objects that outlive their usefulness, global variable accretion, and detached DOM nodes held in JS variables. WeakMap and WeakRef allow holding references that do not prevent GC. Node.js processes have a default heap limit (~1.5 GB on 64-bit) configurable with --max-old-space-size. The Chrome DevTools Memory panel provides heap snapshots, allocation timelines, and retained-size analysis to identify leak sources. In long-lived SPAs and Node.js servers, undetected leaks cause gradual memory growth until OOM.
Watch Out
Common Misconception
Why It Matters
Common Mistakes
- Adding event listeners without removing them on cleanup — each listener holds a closure reference, keeping associated objects alive indefinitely.
- Storing large objects in module-level or global variables — they live for the process lifetime and are never collected.
- Accumulating items in a cache Map without a size limit or TTL — unbounded Maps are the most common Node.js memory leak.
- Holding references to detached DOM nodes in JavaScript arrays or Maps — the nodes cannot be GC'd even though they are not in the document.
Code Examples
// Listener leak — added on every render, never removed:
function attachHandler() {
document.addEventListener('scroll', () => {
processLargeData(globalCache); // holds reference to cache
});
// No removeEventListener — each call adds another permanent listener
}
// Cleanup with AbortController:
function attachHandler() {
const controller = new AbortController();
document.addEventListener('scroll', () => {
processLargeData(globalCache);
}, { signal: controller.signal });
return () => controller.abort(); // call this on unmount/cleanup
}