Module 27: Memory Management and Performance
Understanding memory management and performance optimization is crucial for building fast, efficient JavaScript applications that scale.
1. JavaScript Memory Model
1.1 Stack vs Heap
// Stack (primitive values)
let num = 42; // Stored on stack
let str = 'hello'; // Reference on stack, value on heap
let bool = true; // Stored on stack
// Heap (objects, arrays, functions)
let obj = { x: 1 }; // Reference on stack, object on heap
let arr = [1, 2, 3]; // Reference on stack, array on heap
let func = () => {}; // Reference on stack, function on heap
// Example: Value types vs Reference types
let a = 10;
let b = a; // Copy of value
b = 20;
console.log(a); // 10 (unchanged)
let obj1 = { value: 10 };
let obj2 = obj1; // Copy of reference
obj2.value = 20;
console.log(obj1.value); // 20 (changed!)
// Creating independent copies
let obj3 = { ...obj1 }; // Shallow copy
let obj4 = JSON.parse(JSON.stringify(obj1)); // Deep copy (limited)
let obj5 = structuredClone(obj1); // Deep copy (modern)
1.2 Memory Lifecycle
// 1. Allocation
let obj = { data: new Array(1000) }; // Memory allocated
// 2. Usage
obj.data.push(1); // Using allocated memory
// 3. Release (automatic via garbage collection)
obj = null; // No more references, eligible for GC
JavaScript uses automatic garbage collection. The engine (V8, SpiderMonkey, etc.) periodically frees memory that's no longer reachable.
2. Memory Leaks
2.1 Common Memory Leak Patterns
// ❌ Memory Leak 1: Global variables
function createLeak() {
leaked = 'This is global!'; // No 'var', 'let', or 'const'
// 'leaked' is now a global variable and never GC'd
}
// ✅ Fix
function noLeak() {
'use strict';
let notLeaked = 'This is local';
}
// ❌ Memory Leak 2: Forgotten timers
class Component {
constructor() {
this.data = new Array(10000);
this.intervalId = setInterval(() => {
console.log('Running...');
}, 1000);
}
destroy() {
// Forgot to clear the interval!
// this.data is retained as long as interval exists
}
}
// ✅ Fix
class ComponentFixed {
constructor() {
this.data = new Array(10000);
this.intervalId = setInterval(() => {
console.log('Running...');
}, 1000);
}
destroy() {
clearInterval(this.intervalId);
this.data = null;
}
}
// ❌ Memory Leak 3: Event listeners
class Widget {
constructor(element) {
this.element = element;
this.data = new Array(10000);
this.element.addEventListener('click', () => {
console.log(this.data.length);
});
}
destroy() {
this.element.remove(); // Element removed but listener remains!
}
}
// ✅ Fix
class WidgetFixed {
constructor(element) {
this.element = element;
this.data = new Array(10000);
this.onClick = () => {
console.log(this.data.length);
};
this.element.addEventListener('click', this.onClick);
}
destroy() {
this.element.removeEventListener('click', this.onClick);
this.element.remove();
this.data = null;
}
}
// ❌ Memory Leak 4: Closures retaining large objects
function createClosure() {
const largeData = new Array(1000000).fill('data');
return function smallFunction() {
return largeData.length; // Entire largeData is retained!
};
}
// ✅ Fix: Extract only what you need
function createClosureFixed() {
const largeData = new Array(1000000).fill('data');
const length = largeData.length;
return function smallFunction() {
return length; // Only number is retained
};
}
// ❌ Memory Leak 5: Detached DOM nodes
let button = document.getElementById('myButton');
let elements = { button };
button.remove(); // DOM node removed but still referenced in 'elements'
// ✅ Fix
button.remove();
elements.button = null; // Remove reference
2.2 Detecting Memory Leaks
// Using Chrome DevTools Memory Profiler
class MemoryLeakDemo {
constructor() {
this.listeners = [];
this.data = new Array(100000);
}
addListener(element) {
const listener = () => console.log(this.data.length);
element.addEventListener('click', listener);
this.listeners.push({ element, listener });
}
removeListener(element) {
const index = this.listeners.findIndex(l => l.element === element);
if (index > -1) {
const { listener } = this.listeners[index];
element.removeEventListener('click', listener);
this.listeners.splice(index, 1);
}
}
destroy() {
this.listeners.forEach(({ element, listener }) => {
element.removeEventListener('click', listener);
});
this.listeners = [];
this.data = null;
}
}
// Monitoring memory usage
if (performance.memory) {
console.log({
usedJSHeapSize: performance.memory.usedJSHeapSize,
totalJSHeapSize: performance.memory.totalJSHeapSize,
jsHeapSizeLimit: performance.memory.jsHeapSizeLimit
});
}
Closures are powerful but can inadvertently retain references to large objects. Be mindful of what your closures capture.
3. Performance Optimization
3.1 Optimizing Loops
// ❌ Slow: Accessing length in every iteration
for (let i = 0; i < array.length; i++) {
process(array[i]);
}
// ✅ Fast: Cache the length
const len = array.length;
for (let i = 0; i < len; i++) {
process(array[i]);
}
// ✅ Modern: Use for...of when you don't need the index
for (const item of array) {
process(item);
}
// ❌ Slow: Nested loops with poor complexity
for (let i = 0; i < arr1.length; i++) {
for (let j = 0; j < arr2.length; j++) {
if (arr1[i] === arr2[j]) {
// O(n²) complexity
}
}
}
// ✅ Fast: Use Set for O(1) lookups
const set2 = new Set(arr2);
for (const item of arr1) {
if (set2.has(item)) {
// O(n) complexity
}
}
// Array methods performance
const arr = Array.from({ length: 10000 }, (_, i) => i);
// Fast: for loop
console.time('for');
let sum = 0;
for (let i = 0; i < arr.length; i++) {
sum += arr[i];
}
console.timeEnd('for');
// Slower: forEach
console.time('forEach');
sum = 0;
arr.forEach(n => sum += n);
console.timeEnd('forEach');
// Medium: reduce
console.time('reduce');
sum = arr.reduce((acc, n) => acc + n, 0);
console.timeEnd('reduce');
3.2 Debouncing and Throttling
// Debouncing: Execute after quiet period
function debounce(func, delay) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
// Usage: Search input
const searchInput = document.querySelector('#search');
const search = debounce((value) => {
console.log('Searching for:', value);
// Make API call
}, 300);
searchInput.addEventListener('input', (e) => {
search(e.target.value);
});
// Throttling: Execute at most once per time period
function throttle(func, delay) {
let lastCall = 0;
return function (...args) {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
func.apply(this, args);
}
};
}
// Usage: Scroll event
const handleScroll = throttle(() => {
console.log('Scroll position:', window.scrollY);
}, 100);
window.addEventListener('scroll', handleScroll);
// Advanced: Leading and trailing edge throttle
function throttleAdvanced(func, delay, { leading = true, trailing = true } = {}) {
let timeoutId;
let lastCall = 0;
return function (...args) {
const now = Date.now();
const timeSinceLastCall = now - lastCall;
const execute = () => {
lastCall = now;
func.apply(this, args);
};
if (timeSinceLastCall >= delay) {
if (leading) {
execute();
}
if (trailing) {
clearTimeout(timeoutId);
timeoutId = setTimeout(execute, delay);
}
} else if (trailing) {
clearTimeout(timeoutId);
timeoutId = setTimeout(execute, delay - timeSinceLastCall);
}
};
}
3.3 Lazy Loading and Code Splitting
// Lazy loading images
class LazyLoader {
constructor(selector) {
this.images = document.querySelectorAll(selector);
this.observer = new IntersectionObserver(
(entries) => this.handleIntersection(entries),
{ rootMargin: '50px' }
);
this.images.forEach(img => this.observer.observe(img));
}
handleIntersection(entries) {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.add('loaded');
this.observer.unobserve(img);
}
});
}
}
// Usage: <img data-src="image.jpg" alt="Lazy loaded">
const lazyLoader = new LazyLoader('img[data-src]');
// Dynamic imports (code splitting)
async function loadModule() {
const module = await import('./heavy-module.js');
module.doSomething();
}
// Conditional loading
if (user.isPremium) {
const analytics = await import('./analytics.js');
analytics.track('premium_feature_used');
}
// Route-based code splitting (React example concept)
const routes = {
'/home': () => import('./pages/Home.js'),
'/about': () => import('./pages/About.js'),
'/dashboard': () => import('./pages/Dashboard.js')
};
async function navigateTo(path) {
const loadPage = routes[path];
if (loadPage) {
const page = await loadPage();
page.render();
}
}
Debouncing search inputs, throttling scroll handlers, and lazy loading images can dramatically improve perceived performance.
4. Efficient Data Structures
4.1 Choosing the Right Structure
// Array vs Set for uniqueness
// ❌ Slow: Array with includes
const uniqueArray = [];
for (const item of items) {
if (!uniqueArray.includes(item)) {
uniqueArray.push(item); // O(n) lookup each time
}
}
// ✅ Fast: Set
const uniqueSet = new Set(items); // O(1) lookup
const uniqueArray = [...uniqueSet];
// Object vs Map for key-value pairs
// ❌ Limited: Object
const cache = {};
cache['key1'] = 'value1';
cache[{ id: 1 }] = 'value2'; // [object Object] as key!
// ✅ Flexible: Map
const cache = new Map();
cache.set('key1', 'value1');
cache.set({ id: 1 }, 'value2'); // Objects as keys work!
cache.set(function() {}, 'value3'); // Functions as keys too!
// Performance comparison
const iterations = 100000;
// Object
console.time('Object');
const obj = {};
for (let i = 0; i < iterations; i++) {
obj[`key${i}`] = i;
}
console.timeEnd('Object');
// Map
console.time('Map');
const map = new Map();
for (let i = 0; i < iterations; i++) {
map.set(`key${i}`, i);
}
console.timeEnd('Map');
// WeakMap for automatic cleanup
class DataCache {
constructor() {
this.cache = new WeakMap();
}
associate(element, data) {
this.cache.set(element, data);
// When element is garbage collected, entry is automatically removed
}
get(element) {
return this.cache.get(element);
}
}
// Usage with DOM elements
const cache = new DataCache();
const button = document.createElement('button');
cache.associate(button, { clicks: 0, timestamp: Date.now() });
// When button is removed and no longer referenced,
// the cache entry is automatically cleaned up
4.2 Array Optimization
// Pre-allocating arrays
// ❌ Slow: Dynamic growth
const arr1 = [];
for (let i = 0; i < 1000000; i++) {
arr1.push(i); // Array grows dynamically
}
// ✅ Fast: Pre-allocated
const arr2 = new Array(1000000);
for (let i = 0; i < 1000000; i++) {
arr2[i] = i; // No reallocation needed
}
// Avoiding array holes
// ❌ Sparse array (slower)
const sparse = [];
sparse[1000] = 'value'; // Creates holes [empty × 1000, 'value']
// ✅ Dense array (faster)
const dense = new Array(1001).fill(null);
dense[1000] = 'value';
// Efficient array operations
// ❌ Slow: Creating new arrays repeatedly
let result = [1, 2, 3];
result = result.map(x => x * 2);
result = result.filter(x => x > 2);
result = result.map(x => x + 1);
// ✅ Fast: Single pass
let result = [1, 2, 3].reduce((acc, x) => {
const doubled = x * 2;
if (doubled > 2) {
acc.push(doubled + 1);
}
return acc;
}, []);
// Array pooling for frequent allocations
class ArrayPool {
constructor(size = 10) {
this.pool = Array.from({ length: size }, () => []);
}
acquire() {
return this.pool.pop() || [];
}
release(arr) {
arr.length = 0; // Clear array
this.pool.push(arr);
}
}
const pool = new ArrayPool();
function processData(data) {
const temp = pool.acquire();
// Use temp array
temp.push(...data);
const result = temp.map(x => x * 2);
pool.release(temp);
return result;
}
5. DOM Performance
5.1 Minimizing Reflows and Repaints
// ❌ Slow: Multiple reflows
const element = document.getElementById('myElement');
element.style.width = '100px'; // Reflow
element.style.height = '100px'; // Reflow
element.style.margin = '10px'; // Reflow
// ✅ Fast: Batch updates
element.style.cssText = 'width: 100px; height: 100px; margin: 10px;';
// Or use classes
element.classList.add('styled'); // Single reflow
// ❌ Slow: Reading and writing in sequence
const width = element.offsetWidth; // Read (triggers reflow)
element.style.width = width + 10; // Write
const height = element.offsetHeight; // Read (triggers reflow)
element.style.height = height + 10; // Write
// ✅ Fast: Batch reads, then batch writes
const width = element.offsetWidth;
const height = element.offsetHeight;
element.style.width = width + 10;
element.style.height = height + 10;
// Document fragments for batch insertions
// ❌ Slow: Individual insertions
for (let i = 0; i < 1000; i++) {
const div = document.createElement('div');
div.textContent = `Item ${i}`;
container.appendChild(div); // 1000 reflows!
}
// ✅ Fast: Document fragment
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const div = document.createElement('div');
div.textContent = `Item ${i}`;
fragment.appendChild(div);
}
container.appendChild(fragment); // Single reflow
// Virtual scrolling for large lists
class VirtualList {
constructor(container, items, itemHeight) {
this.container = container;
this.items = items;
this.itemHeight = itemHeight;
this.visibleItems = Math.ceil(container.clientHeight / itemHeight);
this.render();
container.addEventListener('scroll', () => this.render());
}
render() {
const scrollTop = this.container.scrollTop;
const startIndex = Math.floor(scrollTop / this.itemHeight);
const endIndex = Math.min(
startIndex + this.visibleItems + 1,
this.items.length
);
// Only render visible items
const html = this.items
.slice(startIndex, endIndex)
.map((item, i) => `
<div style="height: ${this.itemHeight}px;
position: absolute;
top: ${(startIndex + i) * this.itemHeight}px;">
${item}
</div>
`)
.join('');
this.container.innerHTML = `
<div style="height: ${this.items.length * this.itemHeight}px; position: relative;">
${html}
</div>
`;
}
}
// Usage
const items = Array.from({ length: 10000 }, (_, i) => `Item ${i}`);
new VirtualList(document.getElementById('list'), items, 30);
5.2 Event Delegation
// ❌ Slow: Individual listeners
const buttons = document.querySelectorAll('.button');
buttons.forEach(button => {
button.addEventListener('click', handleClick); // N listeners
});
// ✅ Fast: Single delegated listener
document.getElementById('container').addEventListener('click', (e) => {
if (e.target.matches('.button')) {
handleClick(e);
}
});
// Advanced event delegation
class EventDelegate {
constructor(container) {
this.container = container;
this.handlers = new Map();
this.container.addEventListener('click', (e) => {
for (const [selector, handler] of this.handlers) {
if (e.target.matches(selector)) {
handler(e);
}
}
});
}
on(selector, handler) {
this.handlers.set(selector, handler);
}
off(selector) {
this.handlers.delete(selector);
}
}
// Usage
const delegate = new EventDelegate(document.getElementById('app'));
delegate.on('.button', (e) => console.log('Button clicked'));
delegate.on('.link', (e) => console.log('Link clicked'));
6. Web Workers
6.1 Offloading Heavy Computations
// Main thread
const worker = new Worker('worker.js');
worker.postMessage({ type: 'CALCULATE', data: [1, 2, 3, 4, 5] });
worker.onmessage = (e) => {
console.log('Result from worker:', e.data);
};
worker.onerror = (error) => {
console.error('Worker error:', error);
};
// worker.js
self.onmessage = (e) => {
const { type, data } = e.data;
if (type === 'CALCULATE') {
const result = data.reduce((sum, n) => sum + n, 0);
self.postMessage(result);
}
};
// Advanced: Worker pool
class WorkerPool {
constructor(workerScript, poolSize = 4) {
this.workers = Array.from(
{ length: poolSize },
() => new Worker(workerScript)
);
this.queue = [];
this.available = [...this.workers];
}
execute(data) {
return new Promise((resolve, reject) => {
const task = { data, resolve, reject };
if (this.available.length > 0) {
this.runTask(task);
} else {
this.queue.push(task);
}
});
}
runTask(task) {
const worker = this.available.pop();
const onMessage = (e) => {
task.resolve(e.data);
cleanup();
};
const onError = (error) => {
task.reject(error);
cleanup();
};
const cleanup = () => {
worker.removeEventListener('message', onMessage);
worker.removeEventListener('error', onError);
this.available.push(worker);
if (this.queue.length > 0) {
this.runTask(this.queue.shift());
}
};
worker.addEventListener('message', onMessage);
worker.addEventListener('error', onError);
worker.postMessage(task.data);
}
terminate() {
this.workers.forEach(worker => worker.terminate());
}
}
// Usage
const pool = new WorkerPool('worker.js', 4);
Promise.all([
pool.execute({ type: 'TASK1', data: [1, 2, 3] }),
pool.execute({ type: 'TASK2', data: [4, 5, 6] }),
pool.execute({ type: 'TASK3', data: [7, 8, 9] })
]).then(results => {
console.log('All tasks completed:', results);
});
Web Workers run in separate threads and can't access the DOM. Use them for CPU-intensive tasks like data processing, image manipulation, or complex calculations.
7. Caching Strategies
7.1 Memoization
// Simple memoization
function memoize(fn) {
const cache = new Map();
return function (...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
// Usage
const fibonacci = memoize((n) => {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
});
console.time('First call');
console.log(fibonacci(40)); // Slow
console.timeEnd('First call');
console.time('Second call');
console.log(fibonacci(40)); // Instant!
console.timeEnd('Second call');
// LRU Cache
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.cache = new Map();
}
get(key) {
if (!this.cache.has(key)) return undefined;
const value = this.cache.get(key);
// Move to end (most recently used)
this.cache.delete(key);
this.cache.set(key, value);
return value;
}
put(key, value) {
if (this.cache.has(key)) {
this.cache.delete(key);
} else if (this.cache.size >= this.capacity) {
// Remove least recently used (first item)
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, value);
}
}
// Usage
const cache = new LRUCache(3);
cache.put('a', 1);
cache.put('b', 2);
cache.put('c', 3);
cache.put('d', 4); // 'a' is evicted
console.log(cache.get('a')); // undefined
console.log(cache.get('b')); // 2
7.2 Request Caching
// API call caching with expiration
class APICache {
constructor(ttl = 60000) { // 1 minute default
this.cache = new Map();
this.ttl = ttl;
}
async fetch(url, options = {}) {
const key = url + JSON.stringify(options);
const cached = this.cache.get(key);
if (cached && Date.now() - cached.timestamp < this.ttl) {
return cached.data;
}
const response = await fetch(url, options);
const data = await response.json();
this.cache.set(key, {
data,
timestamp: Date.now()
});
return data;
}
clear() {
this.cache.clear();
}
delete(url) {
for (const key of this.cache.keys()) {
if (key.startsWith(url)) {
this.cache.delete(key);
}
}
}
}
// Usage
const apiCache = new APICache(60000);
// First call: fetches from network
const data1 = await apiCache.fetch('https://api.example.com/users');
// Second call within 1 minute: returns from cache
const data2 = await apiCache.fetch('https://api.example.com/users');
Summary
In this module, you learned:
- ✅ JavaScript memory model (stack vs heap)
- ✅ Identifying and preventing memory leaks
- ✅ Performance optimization techniques
- ✅ Debouncing and throttling
- ✅ Efficient data structures (Set, Map, WeakMap)
- ✅ DOM performance optimization
- ✅ Web Workers for parallel processing
- ✅ Caching strategies and memoization
- ✅ Virtual scrolling and lazy loading
In Module 28, you'll learn about Design Patterns in JavaScript, implementing proven solutions to common programming challenges.
Practice Exercises
- Identify and fix memory leaks in a sample application
- Implement a debounced search with autocomplete
- Create a virtual scrolling component for 10,000+ items
- Build a worker pool for parallel data processing
- Implement an LRU cache with TTL
- Optimize a slow loop-heavy algorithm
- Create an event delegation system
- Build a lazy-loading image gallery