Memory Management
Learn how JavaScript manages memory, how garbage collection works, and how to avoid memory leaks.
Memory Management
JavaScript manages memory automatically. You do not manually allocate or free memory like in C or C++. JavaScript handles it for you through a process called garbage collection.
But automatic memory management does not mean you never have to think about memory. Memory leaks — situations where memory is held longer than needed — are real, common, and can make your application slow or crash over time.
Understanding how JavaScript manages memory helps you write code that does not leak.
The Memory Lifecycle
Every piece of data in a JavaScript program goes through three stages:
JavaScript handles allocation and release automatically. You only deal with the middle part — using the data.
// Allocation — memory is reserved
const name = "Ali"; // string allocated
const user = { name: "Ali" }; // object allocated
const numbers = [1, 2, 3]; // array allocated
// Use — read and write
console.log(user.name);
numbers.push(4);
// Release — happens automatically when variables go out of scope
// or when nothing references the data anymoreThe Stack and the Heap
JavaScript uses two memory regions — the stack and the heap.
Stack — fast, fixed size, stores:
- Primitive values — numbers, strings, booleans,
null,undefined - Function call frames — local variables and parameters
- References to objects (not the objects themselves)
Heap — slower, dynamic size, stores:
- Objects
- Arrays
- Functions
- Any reference type
// Stack — stored directly
let age = 22;
let isLoggedIn = true;
const PI = 3.14;
// Heap — object lives in heap, reference lives in stack
const user = { name: "Ali", age: 22 };
// ↑ reference on stack ↑ object in heapWhen a function is called, a stack frame is created with all its local variables. When the function returns, that frame is removed from the stack — those variables are gone. Objects in the heap stay until the garbage collector removes them.
Garbage Collection
JavaScript uses a garbage collector — a background process that periodically finds and removes objects that are no longer needed.
The main algorithm used in modern JavaScript engines is mark-and-sweep.
Mark and Sweep
- Mark — start from roots (global variables, current call stack) and mark every object reachable from them
- Sweep — remove everything that was not marked — unreachable objects
An object is reachable if it can be accessed from a root through any chain of references. If nothing can reach it — it is garbage and gets collected.
let user = { name: "Ali" };
// user object is reachable — user variable references it
user = null;
// nothing references the object anymore
// it is unreachable — garbage collector removes itReachability — The Core Concept
The garbage collector only removes unreachable objects. As long as something holds a reference to an object, it stays in memory.
// Single reference
let user = { name: "Ali" };
user = null; // unreachable — collected
// Multiple references
let user = { name: "Ali" };
let admin = user; // two references to the same object
user = null; // still reachable through admin
admin = null; // now unreachable — collectedInterconnected objects
function createFamily() {
const parent = { name: "Parent" };
const child = { name: "Child" };
parent.child = child; // parent references child
child.parent = parent; // child references parent
return parent;
}
let family = createFamily();
// family → parent ↔ child — all reachable
family = null;
// nothing references parent anymore
// even though parent and child reference each other —
// neither is reachable from any root
// both get collectedThe garbage collector is smart enough to detect circular references that have no path from a root. Both objects get collected even though they reference each other.
Memory Leaks
A memory leak happens when memory that is no longer needed is never released — because something still holds a reference to it, preventing garbage collection.
JavaScript leaks are subtle. The code works — it just uses more and more memory over time until the browser tab crashes or becomes unresponsive.
Leak 1 — Accidental global variables
function createData() {
// ❌ Missing let/const/var — becomes global
data = new Array(100000).fill("leak");
}
createData();
// data is now a global variable
// it lives forever — never garbage collectedfunction createData() {
// ✅ Properly scoped — collected when function returns
const data = new Array(100000).fill("ok");
}Always declare variables with const or let. Global variables live for the entire lifetime of the page.
Leak 2 — Forgotten event listeners
function setupButton() {
const btn = document.getElementById("btn");
const largeData = new Array(100000).fill("data");
// ❌ Event listener holds reference to largeData through closure
btn.addEventListener("click", () => {
console.log(largeData[0]);
});
}
// Called many times — each call adds a new listener and a new largeData
setupButton();
setupButton();
setupButton();
// Three listeners, three largeData arrays — all in memory foreverfunction setupButton() {
const btn = document.getElementById("btn");
// ✅ Named function — can be removed
function handleClick() {
console.log("clicked");
}
btn.addEventListener("click", handleClick);
// Clean up when done
return () => btn.removeEventListener("click", handleClick);
}
const cleanup = setupButton();
// Later when the component is destroyed
cleanup();Leak 3 — Detached DOM elements
// ❌ Keep reference to removed DOM element
let detachedNode;
function createNode() {
const node = document.createElement("div");
node.textContent = "I will leak";
document.body.appendChild(node);
detachedNode = node; // save reference
document.body.removeChild(node); // remove from DOM
}
createNode();
// node is removed from DOM but detachedNode still references it
// the node and its subtree cannot be garbage collected// ✅ Release the reference when done
function createNode() {
const node = document.createElement("div");
document.body.appendChild(node);
document.body.removeChild(node);
// no external reference kept — node can be collected
}Leak 4 — Closures holding large data
// ❌ Closure keeps large data alive unnecessarily
function processData() {
const largeArray = new Array(1000000).fill("data");
return function() {
// Only needs largeArray[0] but keeps all 1M items alive
return largeArray[0];
};
}
const getFirst = processData(); // largeArray stays in memory// ✅ Extract only what you need
function processData() {
const largeArray = new Array(1000000).fill("data");
const firstItem = largeArray[0]; // extract what we need
return function() {
return firstItem; // only firstItem is kept alive
};
}
const getFirst = processData(); // largeArray is collectedLeak 5 — Forgotten timers
// ❌ setInterval that never stops
function startPolling() {
const data = fetchLargeData();
setInterval(() => {
process(data); // data stays alive because interval holds reference
}, 1000);
}
startPolling();
// interval runs forever — data never collected// ✅ Store the interval ID and clear it when done
function startPolling() {
const data = fetchLargeData();
const intervalId = setInterval(() => {
process(data);
}, 1000);
// Return cleanup function
return () => clearInterval(intervalId);
}
const stopPolling = startPolling();
// When done — clear the interval
stopPolling(); // data can now be collectedLeak 6 — Growing Maps and Sets
// ❌ Cache that grows forever
const cache = new Map();
function cacheResult(key, value) {
cache.set(key, value); // never cleaned up
}
// Called thousands of times — cache grows indefinitely// ✅ Bounded cache — limit size
const MAX_CACHE_SIZE = 100;
const cache = new Map();
function cacheResult(key, value) {
if (cache.size >= MAX_CACHE_SIZE) {
// Remove oldest entry
const firstKey = cache.keys().next().value;
cache.delete(firstKey);
}
cache.set(key, value);
}Or use WeakMap when keys are objects — entries are collected automatically when objects are no longer referenced:
// ✅ WeakMap — automatic cleanup
const cache = new WeakMap();
function cacheForElement(element, data) {
cache.set(element, data);
// When element is removed from DOM and dereferenced
// cache entry is automatically collected
}Detecting Memory Leaks
Chrome DevTools Memory tab
- Open DevTools —
F12 - Go to the Memory tab
- Take a Heap Snapshot — shows all objects in memory
- Perform actions in your app
- Take another snapshot
- Compare — objects that grew significantly may be leaks
Performance monitor
- Open DevTools
- Go to Performance tab
- Click record
- Use your app
- Stop recording
- Look at the JS Heap line — if it keeps growing without coming back down — you have a leak
performance.memory (Chrome only)
console.log(performance.memory);
// {
// jsHeapSizeLimit: 2172649472,
// totalJSHeapSize: 23068672,
// usedJSHeapSize: 18234567 ← watch this number
// }Best Practices for Memory Efficiency
// 1. Always declare variables with const or let
const data = []; // not data = []
// 2. Clean up event listeners when components are destroyed
const cleanup = () => element.removeEventListener("click", handler);
// 3. Clear timers when done
const id = setTimeout(fn, 1000);
clearTimeout(id);
// 4. Null out references when done with large objects
let largeData = processFile(file);
doSomething(largeData);
largeData = null; // allow collection
// 5. Use WeakMap/WeakSet for object metadata
const metadata = new WeakMap(); // auto-cleanup
// 6. Avoid storing large data in closures
function process(data) {
const needed = data.summary; // extract only what's needed
return () => needed; // closure holds only the summary
}
// 7. Limit cache sizes
const cache = new Map();
const MAX = 50;
function addToCache(key, val) {
if (cache.size >= MAX) cache.delete(cache.keys().next().value);
cache.set(key, val);
}A Real Example — Component Cleanup
In single-page applications, components mount and unmount. If you do not clean up, each mount adds more event listeners, timers, and subscriptions — all leaking memory.
class DataDashboard {
constructor(container) {
this.container = container;
this.data = [];
this.pollingInterval = null;
this.resizeHandler = this.handleResize.bind(this);
this.abortController = new AbortController();
this.init();
}
init() {
// Start polling
this.pollingInterval = setInterval(() => {
this.fetchData();
}, 5000);
// Listen for resize
window.addEventListener("resize", this.resizeHandler);
// Fetch with abort signal — can be cancelled
this.fetchData();
}
async fetchData() {
try {
const response = await fetch("/api/data", {
signal: this.abortController.signal // ← can abort this
});
this.data = await response.json();
this.render();
} catch (error) {
if (error.name === "AbortError") return; // expected — we aborted
console.error("Fetch failed:", error);
}
}
handleResize() {
this.render();
}
render() {
this.container.innerHTML = this.data
.map(item => `<div>${item.name}: ${item.value}</div>`)
.join("");
}
// Call this when the component is removed
destroy() {
// Clear the polling interval
clearInterval(this.pollingInterval);
// Remove event listener
window.removeEventListener("resize", this.resizeHandler);
// Cancel any in-flight fetch
this.abortController.abort();
// Release data reference
this.data = null;
console.log("Dashboard cleaned up — no memory leaks");
}
}
const dashboard = new DataDashboard(document.getElementById("dashboard"));
// When navigating away or unmounting
dashboard.destroy();Every resource acquired in init() is released in destroy(). Interval cleared, listener removed, fetch cancelled, data released. Nothing leaks.
Summary
- JavaScript manages memory automatically through garbage collection
- The mark-and-sweep algorithm removes objects that are no longer reachable from any root
- An object is reachable if it can be accessed through any chain of references from a root
- Primitives live on the stack — objects live on the heap
- Common memory leaks — accidental globals, forgotten event listeners, detached DOM nodes, closures holding large data, forgotten timers, unbounded caches
- Use WeakMap and WeakSet for object metadata — entries are collected automatically
- Use Chrome DevTools Memory tab to detect leaks — take heap snapshots and compare
- Always clean up — remove event listeners, clear timers, cancel requests when components unmount
- Extract only what you need from large objects before closing over them in functions