Memory Leaks and Garbage Collection
Answer
JavaScript uses automatic garbage collection to manage memory. The garbage collector periodically identifies and frees memory that is no longer reachable. Understanding this helps prevent memory leaks.
How Garbage Collection Works
Mark and Sweep Algorithm
// 1. Start from "roots" (global objects, stack variables)
// 2. Mark all objects reachable from roots
// 3. Sweep (delete) unmarked objects
// Example:
let user = { name: "John" }; // user references the object
user = null; // Object is now unreachable, will be garbage collected
Common Memory Leaks
1. Accidental Globals
// ❌ Bad: Creates global variable
function leak() {
leakedVariable = "I'm global!"; // Missing 'let' or 'const'
}
// ❌ Bad: 'this' is window in non-strict mode
function leak() {
this.leakedProperty = "Also global!";
}
// ✅ Good: Use strict mode
("use strict");
function safe() {
let localVariable = "I'm local";
}
2. Forgotten Timers
// ❌ Bad: Interval never cleared
function startPolling() {
setInterval(() => {
const data = heavyDataProcessing();
updateUI(data);
}, 1000);
}
// ✅ Good: Store reference and clear
class Poller {
start() {
this.intervalId = setInterval(() => {
this.fetchData();
}, 1000);
}
stop() {
clearInterval(this.intervalId);
}
}
3. DOM Reference Leaks
// ❌ Bad: Reference to removed DOM element
let button = document.getElementById("myButton");
document.body.removeChild(button.parentNode);
// 'button' still holds reference, element not garbage collected
// ✅ Good: Null out references
let button = document.getElementById("myButton");
document.body.removeChild(button.parentNode);
button = null;
// ✅ Even better: Use weak references for caches
const cache = new WeakMap();
4. Closures Retaining References
// ❌ Bad: Closure retains large data
function processData() {
const largeData = new Array(1000000).fill("x");
return function process() {
// Even if we don't use largeData, it's retained
return "done";
};
}
// ✅ Good: Only capture what you need
function processData() {
const largeData = new Array(1000000).fill("x");
const summary = largeData.length; // Extract needed data
return function process() {
return `Processed ${summary} items`;
};
}
5. Event Listeners Not Removed
// ❌ Bad: Listener never removed
class Component {
constructor() {
window.addEventListener("resize", this.onResize);
}
onResize = () => {
console.log("Resized");
};
}
// ✅ Good: Clean up on destroy
class Component {
constructor() {
this.onResize = this.onResize.bind(this);
window.addEventListener("resize", this.onResize);
}
onResize() {
console.log("Resized");
}
destroy() {
window.removeEventListener("resize", this.onResize);
}
}
WeakMap and WeakSet
// Regular Map - holds strong references
const map = new Map();
let obj = { data: "important" };
map.set(obj, "metadata");
obj = null;
// Object still exists! Map holds reference
// WeakMap - holds weak references
const weakMap = new WeakMap();
let obj2 = { data: "important" };
weakMap.set(obj2, "metadata");
obj2 = null;
// Object can be garbage collected!
// Use cases for WeakMap:
// - Caching computed data for objects
// - Private data for classes
// - Metadata without preventing GC
Detecting Memory Leaks
// Chrome DevTools - Memory tab
// 1. Take heap snapshot
// 2. Perform suspicious action
// 3. Take another snapshot
// 4. Compare snapshots
// Monitor memory usage
const used = process.memoryUsage();
console.log({
heapUsed: Math.round(used.heapUsed / 1024 / 1024) + "MB",
heapTotal: Math.round(used.heapTotal / 1024 / 1024) + "MB",
});
// Performance API
const memory = performance.memory;
console.log({
usedJSHeapSize: memory.usedJSHeapSize,
totalJSHeapSize: memory.totalJSHeapSize,
});
Memory-Efficient Patterns
// 1. Object pooling for frequently created objects
class ObjectPool {
constructor(factory, size = 100) {
this.pool = Array.from({ length: size }, factory);
}
acquire() {
return this.pool.pop() || this.factory();
}
release(obj) {
this.pool.push(obj);
}
}
// 2. Streaming large data
async function processLargeFile(file) {
const reader = file.stream().getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
processChunk(value);
// Each chunk is processed and discarded
}
}
// 3. Clearing references
function cleanup() {
this.cache = null;
this.handlers = null;
this.data = null;
}
Memory Leak Checklist
| Issue | Solution |
|---|---|
| Global variables | Use let/const, strict mode |
| Event listeners | Remove on cleanup |
| Timers | Clear intervals/timeouts |
| DOM references | Null out after removal |
| Closures | Only capture needed variables |
| Circular references | Use WeakMap/WeakSet |
| Caches | Implement size limits, LRU |
Key Points
- JavaScript uses mark-and-sweep garbage collection
- Memory leaks occur when references prevent GC
- Common causes: globals, timers, event listeners, closures
- Use WeakMap/WeakSet for caches
- Always clean up in component destroy/unmount
- Use DevTools Memory tab to detect leaks