Advanced JavaScript (ES6+)

Performance Optimization

13 min Lesson 38 of 40

Performance Optimization

Performance optimization is crucial for creating fast, responsive applications. In this lesson, we'll explore techniques to measure, analyze, and improve JavaScript performance.

Performance Measurement

Before optimizing, you need to measure performance accurately:

// 1. Using performance.now() - High-resolution timing const start = performance.now(); // Code to measure for (let i = 0; i < 1000000; i++) { Math.sqrt(i); } const end = performance.now(); console.log(`Operation took ${end - start}ms`); // 2. Using console.time() - Simple timing console.time('myOperation'); // Code to measure const result = heavyComputation(); console.timeEnd('myOperation'); // myOperation: 123.45ms // 3. Performance marks and measures performance.mark('start-fetch'); fetch('https://api.example.com/data') .then(response => response.json()) .then(data => { performance.mark('end-fetch'); performance.measure('fetch-duration', 'start-fetch', 'end-fetch'); const measure = performance.getEntriesByName('fetch-duration')[0]; console.log(`Fetch took: ${measure.duration}ms`); }); // 4. Resource timing API - Measure resource loading window.addEventListener('load', () => { const resources = performance.getEntriesByType('resource'); resources.forEach(resource => { console.log(`${resource.name}: ${resource.duration}ms`); }); }); // 5. Navigation timing - Page load metrics const navigationTiming = performance.getEntriesByType('navigation')[0]; console.log('Page load metrics:', { domContentLoaded: navigationTiming.domContentLoadedEventEnd - navigationTiming.domContentLoadedEventStart, totalLoadTime: navigationTiming.loadEventEnd - navigationTiming.fetchStart, domInteractive: navigationTiming.domInteractive - navigationTiming.fetchStart }); // 6. Benchmark different approaches function benchmark(name, fn, iterations = 10000) { const start = performance.now(); for (let i = 0; i < iterations; i++) { fn(); } const end = performance.now(); const total = end - start; const average = total / iterations; console.log(`${name}: Total: ${total.toFixed(2)}ms Average: ${average.toFixed(4)}ms per iteration `); } // Compare approaches benchmark('Array.push', () => { const arr = []; arr.push(1); }); benchmark('Array literal', () => { const arr = [1]; });
Tip: Always measure performance in production-like environments. Development builds and browser DevTools can affect performance measurements.

Memory Management

Efficient memory usage is essential for performance:

// 1. Understanding garbage collection // JavaScript uses automatic garbage collection // Objects are freed when no references exist // Bad: Creates new objects constantly function processItems(items) { items.forEach(item => { const config = { // New object each iteration format: 'json', timeout: 5000 }; processItem(item, config); }); } // Good: Reuse objects function processItems(items) { const config = { // Single object reused format: 'json', timeout: 5000 }; items.forEach(item => { processItem(item, config); }); } // 2. Object pooling for frequently created objects class ObjectPool { constructor(factory, reset) { this.factory = factory; this.reset = reset; this.pool = []; } acquire() { return this.pool.length > 0 ? this.pool.pop() : this.factory(); } release(obj) { this.reset(obj); this.pool.push(obj); } } // Usage const particlePool = new ObjectPool( () => ({ x: 0, y: 0, vx: 0, vy: 0 }), (particle) => { particle.x = 0; particle.y = 0; particle.vx = 0; particle.vy = 0; } ); // 3. Avoid memory leaks // Bad: Event listeners not removed class Component { constructor() { window.addEventListener('resize', this.handleResize); } handleResize() { // Handle resize } } // Good: Clean up listeners class Component { constructor() { this.handleResize = this.handleResize.bind(this); window.addEventListener('resize', this.handleResize); } destroy() { window.removeEventListener('resize', this.handleResize); } handleResize() { // Handle resize } } // 4. WeakMap for caching without memory leaks const cache = new WeakMap(); function processUser(user) { if (cache.has(user)) { return cache.get(user); } const result = expensiveOperation(user); cache.set(user, result); return result; } // When user object is garbage collected, // cache entry is automatically removed // 5. Monitoring memory usage if (performance.memory) { console.log('Memory usage:', { used: (performance.memory.usedJSHeapSize / 1048576).toFixed(2) + ' MB', total: (performance.memory.totalJSHeapSize / 1048576).toFixed(2) + ' MB', limit: (performance.memory.jsHeapSizeLimit / 1048576).toFixed(2) + ' MB' }); }

Avoiding Memory Leaks

Common patterns that cause memory leaks and how to fix them:

// 1. Forgotten timers // Bad class Widget { constructor() { this.interval = setInterval(() => { this.update(); }, 1000); } update() { // Update widget } } // Good class Widget { constructor() { this.interval = setInterval(() => { this.update(); }, 1000); } destroy() { clearInterval(this.interval); } update() { // Update widget } } // 2. Closures holding large data // Bad function createHandler(data) { const largeArray = new Array(1000000).fill(data); return function handler() { // Only uses data, but holds reference to largeArray console.log(data); }; } // Good function createHandler(data) { return function handler() { console.log(data); }; } // 3. DOM references // Bad const elements = []; function cacheElements() { const divs = document.querySelectorAll('div'); divs.forEach(div => elements.push(div)); } // Elements remain in memory even if removed from DOM // Good function processElements() { const divs = document.querySelectorAll('div'); divs.forEach(div => { // Process immediately, don't store processDiv(div); }); } // 4. Global variables // Bad var cache = {}; // Global variable function getData(id) { if (!cache[id]) { cache[id] = fetchData(id); } return cache[id]; } // Cache grows indefinitely // Good const cache = new Map(); const MAX_CACHE_SIZE = 100; function getData(id) { if (!cache.has(id)) { if (cache.size >= MAX_CACHE_SIZE) { // Remove oldest entry const firstKey = cache.keys().next().value; cache.delete(firstKey); } cache.set(id, fetchData(id)); } return cache.get(id); }
Warning: Memory leaks can accumulate over time, especially in single-page applications. Always clean up resources when components are destroyed.

Optimization Techniques

Practical techniques to improve performance:

// 1. Use efficient loops // Slow: Array.forEach with arrow function items.forEach(item => processItem(item)); // Fast: Traditional for loop for (let i = 0, len = items.length; i < len; i++) { processItem(items[i]); } // 2. Cache array lengths // Bad for (let i = 0; i < array.length; i++) { // array.length is read every iteration } // Good for (let i = 0, len = array.length; i < len; i++) { // Length cached } // 3. Avoid unnecessary work in loops // Bad for (let i = 0; i < items.length; i++) { const config = getConfig(); // Called every iteration processItem(items[i], config); } // Good const config = getConfig(); // Called once for (let i = 0; i < items.length; i++) { processItem(items[i], config); } // 4. Use object lookup instead of switch // Slow function getColor(name) { switch(name) { case 'red': return '#FF0000'; case 'blue': return '#0000FF'; case 'green': return '#00FF00'; // ... many more cases } } // Fast const colors = { red: '#FF0000', blue: '#0000FF', green: '#00FF00' }; function getColor(name) { return colors[name]; } // 5. Batch DOM operations // Bad: Multiple reflows for (let i = 0; i < 100; i++) { const div = document.createElement('div'); div.textContent = `Item ${i}`; document.body.appendChild(div); // Reflow each time } // Good: Single reflow const fragment = document.createDocumentFragment(); for (let i = 0; i < 100; i++) { const div = document.createElement('div'); div.textContent = `Item ${i}`; fragment.appendChild(div); } document.body.appendChild(fragment); // Single reflow // 6. Use event delegation // Bad: Multiple listeners document.querySelectorAll('.button').forEach(button => { button.addEventListener('click', handleClick); }); // Good: Single listener document.addEventListener('click', (e) => { if (e.target.matches('.button')) { handleClick(e); } }); // 7. Avoid forced synchronous layouts // Bad: Causes layout thrashing for (let i = 0; i < elements.length; i++) { elements[i].style.width = elements[i].offsetWidth + 10 + 'px'; // offsetWidth forces layout calculation } // Good: Read then write const widths = elements.map(el => el.offsetWidth); elements.forEach((el, i) => { el.style.width = widths[i] + 10 + 'px'; }); // 8. Use requestAnimationFrame for animations // Bad: Using setTimeout function animate() { updatePosition(); setTimeout(animate, 16); // ~60fps } // Good: Using requestAnimationFrame function animate() { updatePosition(); requestAnimationFrame(animate); } // 9. Optimize string concatenation // Slow: String concatenation in loop let html = ''; for (let i = 0; i < 1000; i++) { html += '<div>' + i + '</div>'; } // Fast: Array join const parts = []; for (let i = 0; i < 1000; i++) { parts.push(`<div>${i}</div>`); } const html = parts.join(''); // 10. Use Web Workers for heavy computations // Main thread const worker = new Worker('worker.js'); worker.postMessage({ data: largeDataset }); worker.onmessage = (e) => { console.log('Result:', e.data); }; // worker.js self.onmessage = (e) => { const result = heavyComputation(e.data); self.postMessage(result); };

Debouncing and Throttling

Control the rate of function execution for performance:

// 1. Debounce - Execute after delay of inactivity function debounce(func, delay) { let timeoutId; return function(...args) { clearTimeout(timeoutId); timeoutId = setTimeout(() => { func.apply(this, args); }, delay); }; } // Usage: Search as user types const searchInput = document.querySelector('#search'); const debouncedSearch = debounce((value) => { performSearch(value); }, 300); searchInput.addEventListener('input', (e) => { debouncedSearch(e.target.value); }); // 2. Throttle - Execute at most once per time period function throttle(func, limit) { let inThrottle; return function(...args) { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => { inThrottle = false; }, limit); } }; } // Usage: Scroll event handling const throttledScroll = throttle(() => { console.log('Scroll position:', window.scrollY); }, 100); window.addEventListener('scroll', throttledScroll); // 3. Advanced throttle with leading and trailing function throttleAdvanced(func, limit, options = {}) { let timeout; let previous = 0; const { leading = true, trailing = true } = options; return function(...args) { const now = Date.now(); if (!previous && !leading) { previous = now; } const remaining = limit - (now - previous); if (remaining <= 0 || remaining > limit) { if (timeout) { clearTimeout(timeout); timeout = null; } previous = now; func.apply(this, args); } else if (!timeout && trailing) { timeout = setTimeout(() => { previous = leading ? Date.now() : 0; timeout = null; func.apply(this, args); }, remaining); } }; } // 4. RequestAnimationFrame-based throttle function rafThrottle(func) { let rafId = null; return function(...args) { if (rafId === null) { rafId = requestAnimationFrame(() => { func.apply(this, args); rafId = null; }); } }; } // Usage: Smooth scroll handling const rafScrollHandler = rafThrottle(() => { updateScrollPosition(); }); window.addEventListener('scroll', rafScrollHandler);

Lazy Loading and Code Splitting

Load code and resources only when needed:

// 1. Dynamic imports (code splitting) // Load module only when needed button.addEventListener('click', async () => { const module = await import('./heavy-feature.js'); module.initialize(); }); // 2. Lazy load images const imageObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; img.src = img.dataset.src; imageObserver.unobserve(img); } }); }); document.querySelectorAll('img[data-src]').forEach(img => { imageObserver.observe(img); }); // HTML: <img data-src="large-image.jpg" alt="Description"> // 3. Route-based code splitting const routes = { '/home': () => import('./pages/home.js'), '/about': () => import('./pages/about.js'), '/contact': () => import('./pages/contact.js') }; async function loadRoute(path) { const loader = routes[path]; if (loader) { const module = await loader(); module.render(); } } // 4. Prefetching // Prefetch resources user might need soon function prefetchResource(url) { const link = document.createElement('link'); link.rel = 'prefetch'; link.href = url; document.head.appendChild(link); } // Prefetch next page when hovering over link document.querySelectorAll('a').forEach(link => { link.addEventListener('mouseenter', () => { prefetchResource(link.href); }); }); // 5. Lazy component initialization class ExpensiveComponent { constructor(element) { this.element = element; this.initialized = false; } init() { if (this.initialized) return; // Heavy initialization this.setupComplexFeatures(); this.loadData(); this.initialized = true; } // Initialize when visible observeVisibility() { const observer = new IntersectionObserver((entries) => { if (entries[0].isIntersecting) { this.init(); observer.disconnect(); } }); observer.observe(this.element); } }

Practice Exercise:

Optimize this inefficient code:

// Inefficient code function updateList(items) { document.querySelector('#list').innerHTML = ''; for (let i = 0; i < items.length; i++) { const li = document.createElement('li'); li.textContent = items[i].name; li.style.color = getColorFromDatabase(items[i].type); document.querySelector('#list').appendChild(li); } }

Optimized version:

function updateList(items) { const list = document.querySelector('#list'); // Cache selector const fragment = document.createDocumentFragment(); // Batch DOM operations // Cache color lookups const colorCache = new Map(); for (let i = 0, len = items.length; i < len; i++) { const item = items[i]; const li = document.createElement('li'); li.textContent = item.name; // Use cached color or fetch once if (!colorCache.has(item.type)) { colorCache.set(item.type, getColorFromDatabase(item.type)); } li.style.color = colorCache.get(item.type); fragment.appendChild(li); } // Single DOM update list.innerHTML = ''; list.appendChild(fragment); }

Best Practices

1. Measure First: - Profile before optimizing - Focus on bottlenecks - Use real-world data 2. Optimize Critical Path: - Minimize main thread work - Defer non-critical code - Optimize initial render 3. Reduce Network Requests: - Bundle and minify code - Use CDN for libraries - Enable compression (gzip/brotli) - Implement caching strategies 4. Optimize Assets: - Compress images - Use modern formats (WebP, AVIF) - Lazy load images - Use responsive images 5. Minimize Reflows/Repaints: - Batch DOM updates - Use CSS transforms - Avoid forced synchronous layouts - Use will-change for animations 6. Use Modern APIs: - IntersectionObserver - ResizeObserver - requestAnimationFrame - Web Workers 7. Code Hygiene: - Remove unused code - Avoid memory leaks - Clean up event listeners - Clear timers and intervals

Summary

In this lesson, you learned:

  • Performance measurement techniques and tools
  • Memory management and garbage collection
  • Common memory leak patterns and solutions
  • Optimization techniques for loops, DOM, and strings
  • Debouncing and throttling for event handling
  • Lazy loading and code splitting strategies
  • Best practices for high-performance JavaScript
Next Up: In the next lesson, we'll explore Modern JavaScript APIs that provide powerful capabilities for web applications!