Advanced JavaScript (ES6+)
Performance Optimization
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!