Suggest an editImprove this articleRefine the answer for “Memory leaks in JS”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**A memory leak is a situation where a program keeps references to objects it no longer needs, so the garbage collector cannot free that memory.** The collector only removes unreachable objects, that is, objects no reference leads to; if a forgotten reference remains somewhere, the object counts as alive and stays on the heap. Classic sources: implicit globals, uncleared `setInterval` timers, listeners never removed with `removeEventListener`, closures over large structures, unbounded caches and references to removed DOM nodes. ```javascript function start() { const data = new Array(1e6).fill(0); setInterval(() => console.log(data.length), 1000); } start(); // data is never released: the interval closure holds it ``` **Key point:** a leak is not a bug in the garbage collector, it is an extra reference you forgot to drop.Shown above the full answer for quick recall.Answer (EN)Image**A memory leak is a situation where a program keeps references to objects it no longer needs, so the garbage collector cannot free them.** In other words, an object is no longer used but stays in memory because of forgotten references, gradually eating up the heap. ## Theory ### TL;DR - A leak is holding unnecessary objects in memory through references nobody cleared. - The JS garbage collector only removes unreachable objects: if even one reference exists, the object is alive. - Main causes: implicit globals, uncleared timers, listeners that were never removed, closures over large data, unbounded caches, references to removed DOM nodes. - Symptoms: the heap grows from snapshot to snapshot, detached DOM nodes appear, the interface slows down over time. - Diagnosis: Chrome DevTools, the Memory tab and a heap snapshot; in Node.js, `--inspect`, `heapdump`, `process.memoryUsage()`. - Prevention: clean up timers and listeners, bound your caches, use `WeakMap` and `WeakSet` with their weak references. ### Quick example ```javascript const btn = document.getElementById('btn'); function handleClick() { console.log('clicked'); } btn.addEventListener('click', handleClick); btn.remove(); // the element is out of the DOM, but the listener stayed // handleClick and btn itself remain in memory: this is a detached DOM node ``` The correct version: remove the listener with `btn.removeEventListener('click', handleClick)` before dropping the node, and clear any outer variables that point to the element. ### How the garbage collector works and where a leak comes from JavaScript manages memory automatically. The garbage collector periodically looks for **reachable** objects, that is, objects you can walk to through a chain of references from the roots (the global object, the call stack, live closures). - Everything with no references to it (unreachable) is removed. - Everything with at least one reference stays in memory. That is exactly where the problem comes from: the object is logically no longer needed, but some reference to it survives, so the collector considers it alive and does not free it. Importantly, the engine cannot tell a needed reference from a forgotten one, so a leak is always a bug in the code, never in the collector. ### Typical causes of leaks | Cause | Example | What happens | | --- | --- | --- | | **Global variables** | `window.obj = { big: 'data' }` | they live until the tab is closed | | **Uncleared timers and intervals** | `setInterval(() => {...}, 1000)` without `clearInterval` | the callback holds its closure and is never released | | **Event listeners** | `element.addEventListener('click', handler)` without `removeEventListener` | the element is gone, the handler stays in memory | | **Closures over large data** | functions capturing a scope with large objects | the collector cannot free that scope | | **Caches and data structures** | a `Map` that grows without evicting old keys | entries are never removed automatically | | **DOM references after removing a node** | `const el = document.getElementById('app'); el.remove();` | the node is out of the DOM, but `el` still holds it | An implicit global: ```javascript function createLeak() { leak = []; // without let / const the variable lands on window for (let i = 0; i < 1000000; i++) leak.push(i); } createLeak(); // leak lives until the tab is closed ``` A timer nobody stops: ```javascript function start() { const data = new Array(1e6).fill('x'); setInterval(() => console.log(data.length), 1000); } start(); // data is never cleared, the callback closure holds it ``` An unbounded cache: ```javascript const cache = new Map(); function heavyCalc(key) { if (cache.has(key)) return cache.get(key); const result = Array(1e5).fill(key); cache.set(key, result); // no entry is ever removed return result; } ``` ### How to detect a memory leak **In the browser (Chrome DevTools):** 1. Open the **Memory** tab and pick **Heap snapshot**. 2. Take a snapshot. 3. Repeat the action you suspect (open and close a modal, navigate between pages) and take a second snapshot 10 to 30 seconds later. 4. Compare the snapshots in Comparison mode: if the object count or the number of `Detached DOM nodes` keeps rising, that is a leak. The **Performance** tab with the Memory checkbox enabled is useful too: it shows whether the heap size returns to its baseline after a collection, or whether the line keeps creeping upwards. **In Node.js:** - `node --inspect` plus Chrome DevTools for a heap snapshot. - Tooling such as `clinic.js`, `node --inspect-brk` and the `heapdump` package. - Periodic monitoring with `process.memoryUsage()` and its `heapUsed` and `rss` metrics. **Symptoms you can see without a profiler:** - The app gradually slows down during a long session. - Memory grows and never comes back down after navigation. - Interface delays, especially during scrolling and rendering. - Frequent and long garbage collection pauses. - The tab eventually crashes with an out of memory message. ### How to prevent leaks | Problem | Solution | | --- | --- | | Global variables | Always declare with `let` and `const`, enable strict mode | | Timers | Call `clearTimeout` and `clearInterval` when stopping | | Event listeners | Detach with `removeEventListener` on unmount | | Closures | Do not keep large objects in the scope of long lived functions | | Caches | Bound the size and evict old entries, or use `WeakMap` / `WeakSet` | | DOM | When removing a node, clear every variable referencing it | | SPA applications | Watch the cleanup in `useEffect`, `onUnmounted` and similar hooks | A separate tool is `WeakMap` and `WeakSet`. They hold **weak references**, so they do not block garbage collection: ```javascript const cache = new WeakMap(); function getData(obj) { if (cache.has(obj)) return cache.get(obj); const data = heavyCalculation(obj); cache.set(obj, data); return data; } ``` If `obj` is not used anywhere else, the collector will remove it automatically even though it is a key in the `WeakMap`. The price: such collections are not iterable, have no `size`, and only objects can be keys. A short summary: | What | Description | | --- | --- | | **Memory leak** | Holding unneeded objects in memory | | **Cause** | References that are never released | | **Consequence** | Growing memory, lag, a crashing tab | | **Diagnosis** | Chrome DevTools, the Memory tab and heap snapshots | | **Fix** | Clean up timers, listeners, caches and DOM references | | **Prevention** | `WeakMap`, `WeakSet`, cleanup hooks | ### Common mistakes 1. **Believing the collector counts references.** Modern engines use reachability (mark and sweep), so cyclic references are not a leak by themselves: two structures pointing at each other but unreachable from outside will be collected. 2. **Removing a listener with an anonymous function.** `removeEventListener('click', () => {...})` does nothing: you need the exact same function reference you passed to `addEventListener`. 3. **Forgetting cleanup in components.** A `useEffect` without a cleanup function leaves subscriptions, timers and `AbortController` instances alive after the component unmounts. 4. **Confusing high memory usage with a leak.** An app may simply hold a large legitimate state; a leak is when memory grows and **does not come back** after a collection. 5. **Treating `WeakMap` as a universal fix.** Only the key is weak; if the value points back at the key or at something large and reachable, nothing is freed. 6. **Drawing conclusions from a single heap snapshot.** A leak is only visible over time: you need at least two snapshots taken after the same scenario and a forced collection. 7. **Closing over an entire large object for the sake of one field.** Copying out the value you need (`const { id } = user`) lets the rest of the structure be collected.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.