docs/beyond/concepts/memory-management.mdx
Why does your web app slow down over time? Why does that single-page application become sluggish after hours of use? The answer often lies in memory management, the invisible system that allocates and frees memory as your code runs.
// Memory is allocated automatically when you create values
const user = { name: 'Alice', age: 30 }; // Object stored in heap
const numbers = [1, 2, 3, 4, 5]; // Array stored in heap
let count = 42; // Primitive stored in stack
// But what happens when these are no longer needed?
// JavaScript handles cleanup automatically... most of the time
Unlike languages like C where you manually allocate and free memory, JavaScript handles this automatically. But "automatic" doesn't mean "worry-free." According to the State of JS 2023 survey, memory management and performance optimization remain among the top pain points reported by JavaScript developers. Understanding how memory management works helps you write faster, more efficient code and avoid the dreaded memory leaks that crash applications.
<Info> **What you'll learn in this guide:** - What memory management is and why it matters for performance - The three phases of the memory lifecycle - How stack and heap memory differ and when each is used - Common memory leak patterns and how to prevent them - How to profile memory usage with Chrome DevTools - Best practices for writing memory-efficient JavaScript </Info>Imagine you're running a storage facility. When customers (your code) need to store items (data), you:
┌─────────────────────────────────────────────────────────────────────────┐
│ MEMORY LIFECYCLE │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ ALLOCATE │ ───► │ USE │ ───► │ RELEASE │ │
│ │ │ │ │ │ │ │
│ │ Reserve │ │ Read/Write │ │ Free memory │ │
│ │ memory │ │ data │ │ when done │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ JavaScript does You do this Garbage collector │
│ this automatically explicitly does this for you │
│ │
└─────────────────────────────────────────────────────────────────────────┘
The tricky part? JavaScript handles allocation and release automatically, which means you don't always know when memory is freed. This can lead to memory building up when you expect it to be cleaned.
Memory management is the process of allocating memory when your program needs it, using that memory, and releasing it when it's no longer needed. In JavaScript, this happens automatically through a system called garbage collection, which monitors objects and frees memory that's no longer reachable by your code.
Every piece of data in your program goes through three phases:
<Steps> <Step title="Allocation"> When you create a variable, object, or function, JavaScript automatically reserves memory to store it.```javascript
// All of these trigger memory allocation
const name = "Alice"; // String allocation
const user = { id: 1, name: "Alice" }; // Object allocation
const items = [1, 2, 3]; // Array allocation
function greet() { return "Hello"; } // Function allocation
```
```javascript
// Using allocated memory
console.log(name); // Read from memory
user.age = 30; // Write to memory
items.push(4); // Modify allocated array
const message = greet(); // Execute function, allocate result
```
```javascript
function processData() {
const tempData = { huge: new Array(1000000) };
// tempData is used here...
return tempData.huge.length;
}
// After processData() returns, tempData is unreachable
// The garbage collector will eventually free that memory
```
JavaScript uses two memory regions with different characteristics. Understanding when each is used helps you write more efficient code.
┌─────────────────────────────────────────────────────────────────────────┐
│ STACK vs HEAP MEMORY │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ STACK (Fast, Ordered) HEAP (Flexible, Unordered) │
│ ┌─────────────────────┐ ┌─────────────────────────────┐ │
│ │ let count = 42 │ │ ┌─────────────────────┐ │ │
│ ├─────────────────────┤ │ │ { name: "Alice" } │ │ │
│ │ let active = true │ │ └─────────────────────┘ │ │
│ ├─────────────────────┤ │ ┌───────────┐ │ │
│ │ let price = 19.99 │ │ │ [1, 2, 3] │ │ │
│ ├─────────────────────┤ │ └───────────┘ │ │
│ │ (reference to obj)──┼────────────┼──►┌─────────────────┐ │ │
│ └─────────────────────┘ │ │ { id: 1 } │ │ │
│ │ └─────────────────┘ │ │
│ Primitives stored directly │ │ │
│ References point to heap └─────────────────────────────┘ │
│ Objects stored here │
└─────────────────────────────────────────────────────────────────────────┘
The stack is a fast, ordered region of memory used for:
null, undefined, symbols, BigIntfunction calculateTotal(price, quantity) {
// These primitives are stored on the stack
const tax = 0.08;
const subtotal = price * quantity;
const total = subtotal + (subtotal * tax);
return total;
}
// When the function returns, stack memory is immediately reclaimed
Characteristics:
The heap is a larger, unstructured region used for:
function createUser(name) {
// This object is allocated in the heap
const user = {
name: name,
createdAt: new Date(),
preferences: []
};
return user; // Reference returned, object persists in heap
}
const alice = createUser("Alice");
// The object still exists in heap memory, referenced by 'alice'
Characteristics:
When you assign an object to a variable, the variable holds a reference (like an address) pointing to the object in the heap:
const original = { value: 1 }; // Object in heap, reference in stack
const copy = original; // Same reference, same object!
copy.value = 2;
console.log(original.value); // 2 — both point to the same object
// To create an independent copy:
const independent = { ...original };
independent.value = 3;
console.log(original.value); // Still 2 — different objects
JavaScript automatically allocates memory when you create values. Here's what triggers allocation:
// Primitive allocation
const n = 123; // Allocates memory for a number
const s = "hello"; // Allocates memory for a string
const b = true; // Allocates memory for a boolean
// Object allocation
const obj = { a: 1, b: 2 }; // Allocates memory for object and values
const arr = [1, 2, 3]; // Allocates memory for array and elements
const fn = function() {}; // Allocates memory for function object
// Allocation via operations
const s2 = s.substring(0, 3); // New string allocated
const arr2 = arr.concat([4, 5]); // New array allocated
const obj2 = { ...obj, c: 3 }; // New object allocated
const date = new Date(); // Allocates Date object
const regex = new RegExp("pattern"); // Allocates RegExp object
const map = new Map(); // Allocates Map object
const set = new Set([1, 2, 3]); // Allocates Set and stores values
// Each of these allocates new objects
const div = document.createElement('div'); // Allocates DOM element
const text = document.createTextNode('Hi'); // Allocates text node
const fragment = document.createDocumentFragment();
JavaScript uses garbage collection to automatically free memory that's no longer needed. The key concept is reachability.
A value is reachable if it can be accessed somehow, starting from "root" values:
Roots include:
// Global variable — always reachable
let globalUser = { name: "Alice" };
function example() {
// Local variable — reachable while function executes
const localData = { value: 42 };
// Nested function can access outer variables
function inner() {
console.log(localData.value); // localData is reachable here
}
inner();
} // After example() returns, localData becomes unreachable
Modern JavaScript engines use the mark-and-sweep algorithm. As MDN documents, this approach replaced the older reference-counting strategy because it correctly handles circular references — a limitation that caused notorious memory leaks in early Internet Explorer versions:
┌─────────────────────────────────────────────────────────────────────────┐
│ MARK-AND-SWEEP ALGORITHM │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Step 1: MARK Step 2: SWEEP │
│ ───────────── ──────────── │
│ Start from roots, Remove all objects │
│ mark all reachable objects that weren't marked │
│ │
│ [root] [root] │
│ │ │ │
│ ▼ ▼ │
│ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │ A │ ───► │ B │ │ A │ ───► │ B │ │
│ │ ✓ │ │ ✓ │ │ │ │ │ │
│ └─────┘ └─────┘ └─────┘ └─────┘ │
│ │
│ ┌─────┐ ┌─────┐ ╳─────╳ ╳─────╳ │
│ │ C │ ───► │ D │ │ DEL │ │ DEL │ │
│ │ │ │ │ ╳─────╳ ╳─────╳ │
│ └─────┘ └─────┘ (Unreachable = deleted) │
│ (Not reachable from root) │
│ │
└─────────────────────────────────────────────────────────────────────────┘
user = null; // Now the object has no references
// The garbage collector will eventually free it
```
user = null; // Object still reachable via 'admin'
admin = null; // Now the object is unreachable
// Garbage collector can free it
```
let family = marry({ name: "John" }, { name: "Ann" });
family = null; // The entire structure becomes unreachable
// Even though John and Ann reference each other,
// they're unreachable from any root — so they're freed
```
A memory leak occurs when your application retains memory that's no longer needed. Over time, this causes performance degradation and eventually crashes. Here are the most common causes:
// ❌ BAD: Creating global variables accidentally
function processData() {
// Forgot 'const' — this creates a global variable!
leakedData = new Array(1000000);
}
// ✅ GOOD: Use proper variable declarations
function processData() {
const localData = new Array(1000000);
// localData is freed when function returns
}
// ✅ BETTER: Use strict mode to catch this error
"use strict";
function processData() {
leakedData = []; // ReferenceError: leakedData is not defined
}
// ❌ BAD: Interval never cleared
function startPolling() {
const data = fetchHugeData();
setInterval(() => {
// This closure keeps 'data' alive forever!
console.log(data.length);
}, 1000);
}
// ✅ GOOD: Store interval ID and clear when done
function startPolling() {
const data = fetchHugeData();
const intervalId = setInterval(() => {
console.log(data.length);
}, 1000);
// Return cleanup function
return () => clearInterval(intervalId);
}
const stopPolling = startPolling();
// Later, when done:
stopPolling();
// ❌ BAD: Keeping references to removed DOM elements
const elements = [];
function addElement() {
const div = document.createElement('div');
document.body.appendChild(div);
elements.push(div); // Reference stored
}
function removeElement() {
const div = elements[0];
document.body.removeChild(div); // Removed from DOM
// But still referenced in 'elements' array — memory leak!
}
// ✅ GOOD: Remove references when removing elements
function removeElement() {
const div = elements.shift(); // Remove from array
document.body.removeChild(div); // Remove from DOM
// Now the element can be garbage collected
}
// ❌ BAD: Closure keeps large data alive
function createHandler() {
const hugeData = new Array(1000000).fill('x');
return function handler() {
// Even if we only use hugeData.length,
// the entire array is kept in memory
console.log(hugeData.length);
};
}
const handler = createHandler();
// hugeData cannot be garbage collected while handler exists
// ✅ GOOD: Only capture what you need
function createHandler() {
const hugeData = new Array(1000000).fill('x');
const length = hugeData.length; // Extract needed value
return function handler() {
console.log(length); // Only captures 'length'
};
}
// hugeData can be garbage collected after createHandler returns
// ❌ BAD: Event listeners keep elements and handlers in memory
class Component {
constructor(element) {
this.element = element;
this.data = fetchLargeData();
this.handleClick = () => {
console.log(this.data);
};
element.addEventListener('click', this.handleClick);
}
// No cleanup method!
}
// ✅ GOOD: Always provide cleanup
class Component {
constructor(element) {
this.element = element;
this.data = fetchLargeData();
this.handleClick = () => {
console.log(this.data);
};
element.addEventListener('click', this.handleClick);
}
destroy() {
this.element.removeEventListener('click', this.handleClick);
this.element = null;
this.data = null;
}
}
// ❌ BAD: Unbounded cache
const cache = {};
function getData(key) {
if (!cache[key]) {
cache[key] = expensiveOperation(key);
}
return cache[key];
}
// Cache grows forever!
// ✅ GOOD: Use WeakMap for object keys (auto-cleanup)
const cache = new WeakMap();
function getData(obj) {
if (!cache.has(obj)) {
cache.set(obj, expensiveOperation(obj));
}
return cache.get(obj);
}
// When obj is garbage collected, its cache entry is too
// ✅ ALSO GOOD: Bounded LRU cache for string keys
class LRUCache {
constructor(maxSize = 100) {
this.cache = new Map();
this.maxSize = maxSize;
}
get(key) {
if (this.cache.has(key)) {
// Move to end (most recently used)
const value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value);
return value;
}
return undefined;
}
set(key, value) {
if (this.cache.has(key)) {
this.cache.delete(key);
} else if (this.cache.size >= this.maxSize) {
// Delete oldest entry
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, value);
}
}
JavaScript provides special data structures designed for memory efficiency:
WeakMap and WeakSet, introduced in the ECMAScript 2015 specification, hold "weak" references that don't prevent garbage collection:
// WeakMap: Associate data with objects without preventing GC
const metadata = new WeakMap();
function processElement(element) {
metadata.set(element, {
processedAt: Date.now(),
clickCount: 0
});
}
// When 'element' is removed from DOM and dereferenced,
// its WeakMap entry is automatically removed too
// Regular Map would keep the element alive:
const regularMap = new Map();
regularMap.set(element, data);
// Even after element is removed, regularMap keeps it in memory!
When to use:
WeakRef provides a weak reference to an object, allowing you to check if it still exists:
// Use case: Cache that allows garbage collection
const cache = new Map();
function getCached(key, compute) {
if (cache.has(key)) {
const ref = cache.get(key);
const value = ref.deref(); // Get object if it still exists
if (value !== undefined) {
return value;
}
}
const value = compute();
cache.set(key, new WeakRef(value));
return value;
}
Chrome DevTools provides powerful tools for finding memory issues.
Detached DOM nodes are elements removed from the document but still referenced in JavaScript:
// This creates a detached DOM tree
let detachedNodes = [];
function leakMemory() {
const div = document.createElement('div');
div.innerHTML = '<span>Lots of content...</span>'.repeat(1000);
detachedNodes.push(div); // Never added to DOM, but kept in array
}
To find them in DevTools:
Detached HTMLDivElement, etc.To find memory leaks:
```javascript
function processLargeData() {
let data = loadHugeDataset();
const result = analyze(data);
data = null; // Allow GC to free the dataset
return result;
}
```
```javascript
class ParticlePool {
constructor(size) {
this.pool = Array.from({ length: size }, () => ({
x: 0, y: 0, vx: 0, vy: 0, active: false
}));
}
acquire() {
const particle = this.pool.find(p => !p.active);
if (particle) particle.active = true;
return particle;
}
release(particle) {
particle.active = false;
}
}
```
```javascript
// ❌ BAD: Creates new function every iteration
items.forEach(item => {
element.addEventListener('click', () => handle(item));
});
// ✅ GOOD: Create handler once
function createHandler(item) {
return () => handle(item);
}
// Or use a single delegated handler
container.addEventListener('click', (e) => {
const item = e.target.closest('[data-item]');
if (item) handle(item.dataset.item);
});
```
```javascript
// ❌ BAD: Creates many intermediate strings
let result = '';
for (let i = 0; i < 10000; i++) {
result += 'item ' + i + ', ';
}
// ✅ GOOD: Build array, join once
const parts = [];
for (let i = 0; i < 10000; i++) {
parts.push(`item ${i}`);
}
const result = parts.join(', ');
```
```javascript
// React example
useEffect(() => {
const subscription = dataSource.subscribe(handleData);
return () => {
subscription.unsubscribe(); // Cleanup on unmount
};
}, []);
```
JavaScript manages memory automatically — You don't allocate or free memory manually, but you must understand how it works to avoid leaks
Memory lifecycle has three phases — Allocation (automatic), use (your code), and release (garbage collection)
Stack is for primitives, heap is for objects — Primitives and references live on the stack; objects live on the heap
Reachability determines garbage collection — Objects are freed when they can't be reached from roots (global variables, current function stack)
Mark-and-sweep is the algorithm — The GC marks all reachable objects, then sweeps away the rest
Common leaks: globals, timers, DOM refs, closures, listeners — These patterns keep objects reachable unintentionally
WeakMap and WeakSet prevent leaks — They hold weak references that don't prevent garbage collection
DevTools Memory panel finds leaks — Use heap snapshots and comparisons to identify retained objects
Explicitly null references to large objects — Help the GC by breaking references when you're done
Clean up event listeners and timers — Always remove listeners and clear intervals when components unmount
</Info>1. **Allocation** — Memory is reserved when you create values (automatic in JavaScript)
2. **Use** — Your code reads and writes to allocated memory
3. **Release** — Memory is freed when no longer needed (handled by garbage collection)
In JavaScript, phases 1 and 3 are automatic, while phase 2 is controlled by your code.
| Stack | Heap |
|-------|------|
| Stores primitives and references | Stores objects |
| Fixed size, fast access | Dynamic size, slower access |
| LIFO order, auto-managed | Managed by garbage collector |
| Freed when function returns | Freed when unreachable |
```javascript
function example() {
const num = 42; // Stack (primitive)
const obj = { x: 1 }; // Heap (object)
const ref = obj; // Stack (reference to heap object)
}
```
buttons.forEach(button => {
const handler = () => console.log('clicked');
button.addEventListener('click', handler);
handlers.push(handler);
});
```
**Answer:** This code causes a memory leak because:
1. Event listeners are never removed
2. Handler functions are stored in the `handlers` array
3. Even if buttons are removed from the DOM, they can't be garbage collected because:
- The `handlers` array keeps references to the handler functions
- The handler functions are attached to the buttons
**Fix:** Remove event listeners and clear the array when buttons are removed:
```javascript
function cleanup() {
buttons.forEach((button, i) => {
button.removeEventListener('click', handlers[i]);
});
handlers.length = 0;
}
```
1. **Keys are objects** that may be garbage collected
2. **You're associating metadata** with objects owned by other code
3. **You don't want your map to prevent garbage collection** of the keys
```javascript
// Storing computed data for DOM elements
const elementData = new WeakMap();
function processElement(el) {
if (!elementData.has(el)) {
elementData.set(el, computeExpensiveData(el));
}
return elementData.get(el);
}
// When el is removed from DOM and dereferenced,
// its entry in elementData is automatically cleaned up
```
Use regular `Map` when you need to iterate over entries or when keys are primitives.
Common causes:
- Storing DOM elements in arrays or objects
- Event handlers that reference removed elements via closures
- Caches that hold DOM element references
To find them:
1. Take a heap snapshot in DevTools Memory panel
2. Filter for "Detached"
3. Examine what's retaining each detached element
4. Remove those references in your code
**Mark phase:**
1. Start from "roots" (global variables, current call stack)
2. Visit all objects reachable from roots
3. Mark each visited object as "alive"
4. Follow references to mark objects reachable from marked objects
5. Continue until all reachable objects are marked
**Sweep phase:**
1. Scan through all objects in memory
2. Delete any object that wasn't marked
3. Reclaim that memory for future allocations
This approach handles circular references correctly — if two objects reference each other but neither is reachable from a root, both are collected.