Skip to main content

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
Garbage Collection

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
});
}
Watch Out for Closures

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();
}
}
Performance Wins

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

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
Next Steps

In Module 28, you'll learn about Design Patterns in JavaScript, implementing proven solutions to common programming challenges.


Practice Exercises

  1. Identify and fix memory leaks in a sample application
  2. Implement a debounced search with autocomplete
  3. Create a virtual scrolling component for 10,000+ items
  4. Build a worker pool for parallel data processing
  5. Implement an LRU cache with TTL
  6. Optimize a slow loop-heavy algorithm
  7. Create an event delegation system
  8. Build a lazy-loading image gallery

Additional Resources