A garbage-collected language cannot leak memory that is unreachable. A leak in JavaScript is data you will never use again that is still reachable, so the collector must keep it. The usual retainers:
- Unbounded caches and collections. A
Map, array or object that only ever grows (per-request, per-user, per-id). - Event listeners never removed. An emitter or DOM node holds the handler, the handler's closure holds its data. Anonymous handlers cannot be removed later (MDN).
- Timers. A live
setIntervalcallback keeps its closure reachable untilclearInterval. - Closures that capture more than they use (see the closure-memory question).
- Detached DOM nodes. An element removed from the page but still referenced from JavaScript, along with its subtree (Chrome's definition).
- Accidental globals and module-level state that lives for the whole process.
Finding one.
- Confirm it is a leak: memory climbs across repeated identical work and does not fall after collection. In Node, compare
process.memoryUsage().heapUsedafterglobal.gc()(run with--expose-gc). In Chrome, use the Task Manager or the Performance panel's Memory checkbox. - Locate it with the Memory panel: take a heap snapshot, repeat the action, take another, and use the Comparison view. A detached class filter finds orphaned DOM. An allocation timeline shows blue bars (allocated and still alive) for objects that are not being released. Read the Retainers pane to see the path from a root.
- Node:
v8.writeHeapSnapshot(),--heapsnapshot-signal=SIGUSR2, or--heapsnapshot-near-heap-limit=N, then load the.heapsnapshotfile in DevTools. - Fix the retainer (remove the listener, clear the timer, bound the cache) and re-measure.
Heap limit. V8's old generation has a cap. Hitting it aborts the process with FATAL ERROR: Reached heap limit … JavaScript heap out of memory. --max-old-space-size=<MiB> changes it; that buys time, not a fix.