JavaScript Memory Management and Garbage Collection
Learn how JavaScript manages memory, understand garbage collection algorithms, and discover best practices to prevent memory leaks in your applications.
Understanding memory management in JavaScript is crucial for building performant applications. While JavaScript handles memory allocation and deallocation automatically, knowing how it works helps you write more efficient code and avoid memory leaks.
Memory Life Cycle
The memory life cycle in JavaScript consists of three phases:
- Allocation: Memory is allocated when you create variables, objects, or functions
- Usage: Reading and writing to allocated memory
- Release: Removing unused memory (garbage collection)
Memory Allocation
Stack vs Heap
JavaScript uses two types of memory:
// Stack memory - primitives
let age = 30; // number stored on stack
let name = 'John'; // string stored on stack
let isActive = true; // boolean stored on stack
// Heap memory - objects and functions
let user = {
// object stored on heap
name: 'John',
age: 30,
};
let numbers = [1, 2, 3]; // array stored on heap
function greet() {
// function stored on heap
console.log('Hello!');
}
Dynamic Memory Allocation
// Memory is allocated when objects are created
let user = {
name: 'Alice',
preferences: {
theme: 'dark',
language: 'en',
},
};
// Adding properties allocates more memory
user.email = 'alice@example.com';
user.scores = [95, 87, 92];
// Creating new objects
let users = [];
for (let i = 0; i < 1000; i++) {
users.push({
id: i,
name: `User ${i}`,
data: new Array(100).fill(i),
});
}
Garbage Collection
JavaScript uses automatic garbage collection to free up memory that's no longer being used.
Mark and Sweep Algorithm
The most common garbage collection algorithm:
// Example of reachable and unreachable objects
let globalUser = { name: 'Global' }; // Reachable from global
function createUser() {
let localUser = { name: 'Local' }; // Reachable within function
return function () {
console.log(localUser.name); // localUser is captured in closure
};
}
let closure = createUser(); // localUser remains reachable
// Unreachable object - will be garbage collected
function createTemp() {
let temp = { data: 'temporary' };
// temp becomes unreachable when function ends
}
createTemp(); // temp is now eligible for garbage collection
Reference Counting (Legacy)
Older JavaScript engines used reference counting:
// Reference counting example
let obj1 = { name: 'Object 1' }; // Reference count: 1
let obj2 = obj1; // Reference count: 2
obj1 = null; // Reference count: 1
obj2 = null; // Reference count: 0 - can be collected
// Circular reference problem
function circularReference() {
let objA = {};
let objB = {};
objA.ref = objB; // objB reference count: 1
objB.ref = objA; // objA reference count: 1
// Even when function ends, objects reference each other
// Modern mark-and-sweep handles this correctly
}
Common Memory Leaks
1. Accidental Global Variables
// Bad - creates global variable
function setUser() {
user = { name: 'John' }; // Missing 'let', 'const', or 'var'
}
// Good - proper declaration
function setUser() {
let user = { name: 'John' };
return user;
}
// Strict mode prevents accidental globals
('use strict');
function setUser() {
user = { name: 'John' }; // Error in strict mode
}
2. Forgotten Timers and Callbacks
// Memory leak - timer never cleared
let data = { large: new Array(1000000).fill('data') };
setInterval(() => {
console.log(data.large.length);
// data can never be garbage collected
}, 1000);
// Good - clear timer when done
let intervalId = setInterval(() => {
console.log(data.large.length);
}, 1000);
// Clear when no longer needed
clearInterval(intervalId);
data = null; // Now can be garbage collected
3. DOM References
// Memory leak - detached DOM nodes
let elements = {
button: document.getElementById('button'),
container: document.getElementById('container'),
};
// Remove from DOM but reference still exists
document.body.removeChild(document.getElementById('container'));
// elements.container still references the removed node
// Good - clear references
elements.container = null;
// Or better - use weak references
let weakElements = new WeakMap();
let button = document.getElementById('button');
weakElements.set(button, { clicks: 0 });
// When button is removed from DOM and no other references exist,
// it can be garbage collected
4. Closures
// Potential memory leak with closures
function createLargeClosure() {
let largeData = new Array(1000000).fill('data');
return function () {
console.log(largeData.length);
// largeData is kept in memory as long as closure exists
};
}
let closure = createLargeClosure();
// largeData cannot be garbage collected until closure = null
// Better approach - only capture what's needed
function createSmallClosure() {
let largeData = new Array(1000000).fill('data');
let dataLength = largeData.length; // Capture only the length
return function () {
console.log(dataLength);
// largeData can be garbage collected after function returns
};
}
5. Event Listeners
// Memory leak - event listener not removed
class Button {
constructor() {
this.handleClick = this.handleClick.bind(this);
document
.getElementById('button')
.addEventListener('click', this.handleClick);
}
handleClick() {
console.log('Clicked');
}
// Missing cleanup
}
// Good - proper cleanup
class Button {
constructor() {
this.element = document.getElementById('button');
this.handleClick = this.handleClick.bind(this);
this.element.addEventListener('click', this.handleClick);
}
handleClick() {
console.log('Clicked');
}
destroy() {
this.element.removeEventListener('click', this.handleClick);
this.element = null;
}
}
Memory Management Best Practices
1. Use Appropriate Scope
// Bad - unnecessary global scope
let tempData = [];
function processData() {
tempData = fetchData();
// Process tempData
return transformData(tempData);
}
// Good - local scope
function processData() {
let tempData = fetchData();
// Process tempData
return transformData(tempData);
// tempData is eligible for GC after function returns
}
2. Clear References
// Clear large objects when done
let cache = {
users: new Array(10000).fill({ name: 'User' }),
posts: new Array(50000).fill({ content: 'Post' }),
};
// Use the cache
processCache(cache);
// Clear when done
cache.users = null;
cache.posts = null;
// Or
cache = null;
3. Use WeakMap and WeakSet
// WeakMap for metadata
let metadata = new WeakMap();
function attachMetadata(object, data) {
metadata.set(object, data);
}
let obj = { name: 'Important' };
attachMetadata(obj, { created: Date.now() });
// When obj is no longer referenced elsewhere
obj = null;
// Both obj and its metadata can be garbage collected
// WeakSet for tracking
let activeConnections = new WeakSet();
class Connection {
constructor() {
activeConnections.add(this);
}
close() {
activeConnections.delete(this);
// Connection can be GC'd when no other references exist
}
}
4. Manage Event Listeners
// Use AbortController for cleanup
function setupListeners() {
const controller = new AbortController();
const { signal } = controller;
document.addEventListener('click', handleClick, { signal });
window.addEventListener('resize', handleResize, { signal });
// Cleanup all listeners at once
return () => controller.abort();
}
const cleanup = setupListeners();
// Later...
cleanup(); // Removes all listeners
Memory Profiling
Using Browser DevTools
// Mark points for profiling
console.time('operation');
// Create objects
let data = [];
for (let i = 0; i < 100000; i++) {
data.push({
id: i,
value: Math.random(),
nested: {
data: new Array(10).fill(i),
},
});
}
console.timeEnd('operation');
// Take heap snapshot in DevTools
// Perform operation
// Take another snapshot
// Compare to find memory leaks
Memory Usage Monitoring
// Monitor memory usage (Chrome/Node.js)
if (performance.memory) {
console.log({
totalJSHeapSize: performance.memory.totalJSHeapSize,
usedJSHeapSize: performance.memory.usedJSHeapSize,
jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
});
}
// Custom memory monitoring
class MemoryMonitor {
constructor() {
this.measurements = [];
}
measure(label) {
if (performance.memory) {
this.measurements.push({
label,
timestamp: Date.now(),
memory: performance.memory.usedJSHeapSize,
});
}
}
report() {
return this.measurements.map((m, i) => {
if (i === 0) return m;
const diff = m.memory - this.measurements[i - 1].memory;
return {
...m,
difference: diff,
formatted: `${(diff / 1024 / 1024).toFixed(2)} MB`,
};
});
}
}
Optimization Strategies
Object Pooling
// Reuse objects instead of creating new ones
class ObjectPool {
constructor(createFn, resetFn, maxSize = 100) {
this.createFn = createFn;
this.resetFn = resetFn;
this.pool = [];
this.maxSize = maxSize;
}
acquire() {
if (this.pool.length > 0) {
return this.pool.pop();
}
return this.createFn();
}
release(obj) {
if (this.pool.length < this.maxSize) {
this.resetFn(obj);
this.pool.push(obj);
}
}
}
// Usage
const particlePool = new ObjectPool(
() => ({ x: 0, y: 0, velocity: { x: 0, y: 0 } }),
(particle) => {
particle.x = 0;
particle.y = 0;
particle.velocity.x = 0;
particle.velocity.y = 0;
}
);
Lazy Loading
// Load data only when needed
class DataManager {
constructor() {
this.cache = new Map();
}
async getData(key) {
if (!this.cache.has(key)) {
const data = await this.loadData(key);
this.cache.set(key, data);
// Optional: Implement LRU cache
if (this.cache.size > 100) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
}
return this.cache.get(key);
}
async loadData(key) {
// Simulate data loading
return fetch(`/api/data/${key}`).then((r) => r.json());
}
clear() {
this.cache.clear();
}
}
Conclusion
While JavaScript's automatic memory management simplifies development, understanding how it works is essential for building efficient applications. By following best practices and avoiding common pitfalls, you can prevent memory leaks and ensure your applications perform well even under heavy load. Regular profiling and monitoring help identify issues before they impact users.