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​