JavaScript Performance Optimization - Complete Guide
Learn advanced techniques to optimize JavaScript performance including memory management, DOM optimization, async patterns, and profiling tools.
JavaScript Performance Optimization - Complete Guide
Performance optimization is crucial for creating fast, responsive web applications. This comprehensive guide covers advanced techniques to optimize JavaScript performance, from memory management to asynchronous patterns.
1. Memory Management #
Understanding Memory Leaks #
Memory leaks occur when objects are no longer needed but can't be garbage collected due to unwanted references.
// Common memory leak patterns
// 1. Global variables
function createLeak() {
// Accidentally creates global variable
leakedVariable = 'This creates a global variable';
}
// 2. Forgotten timers
function startTimer() {
const data = new Array(1000000).fill('data');
setInterval(() => {
// This closure keeps 'data' in memory forever
console.log('Timer running');
}, 1000);
}
// 3. DOM references
let elements = [];
function addElement() {
const div = document.createElement('div');
document.body.appendChild(div);
elements.push(div); // Keeps reference even after removal
}
// 4. Event listeners
function attachListeners() {
const button = document.getElementById('myButton');
const heavyObject = new Array(1000000).fill('data');
button.addEventListener('click', () => {
// This closure keeps heavyObject in memory
console.log('Button clicked');
});
}
Best Practices for Memory Management #
// Good practices to prevent memory leaks
// 1. Use const and let instead of var
function goodScope() {
const data = 'This is block-scoped';
let counter = 0;
// Variables are automatically cleaned up
}
// 2. Clean up timers and intervals
class Timer {
constructor() {
this.intervalId = null;
}
start() {
this.intervalId = setInterval(() => {
console.log('Timer tick');
}, 1000);
}
stop() {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
}
}
// 3. Use WeakMap and WeakSet for object associations
const objectData = new WeakMap();
function associateData(obj, data) {
objectData.set(obj, data);
// When obj is garbage collected, the association is automatically removed
}
// 4. Remove event listeners
class Component {
constructor() {
this.handleClick = this.handleClick.bind(this);
}
mount() {
document.addEventListener('click', this.handleClick);
}
unmount() {
document.removeEventListener('click', this.handleClick);
}
handleClick(event) {
console.log('Clicked:', event.target);
}
}
// 5. Use AbortController for fetch requests
class ApiClient {
constructor() {
this.controller = new AbortController();
}
async fetchData(url) {
try {
const response = await fetch(url, {
signal: this.controller.signal
});
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
console.log('Request aborted');
} else {
throw error;
}
}
}
cancel() {
this.controller.abort();
}
}
2. DOM Optimization #
Minimize DOM Manipulations #
// Inefficient DOM manipulation
function inefficientDOM() {
const list = document.getElementById('list');
for (let i = 0; i < 1000; i++) {
const item = document.createElement('li');
item.textContent = `Item ${i}`;
list.appendChild(item); // Causes reflow for each item
}
}
// Efficient DOM manipulation
function efficientDOM() {
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const item = document.createElement('li');
item.textContent = `Item ${i}`;
fragment.appendChild(item);
}
document.getElementById('list').appendChild(fragment); // Single reflow
}
// Batch DOM operations
function batchDOMOperations() {
const element = document.getElementById('target');
// Bad: multiple style changes
element.style.width = '100px';
element.style.height = '100px';
element.style.backgroundColor = 'red';
// Good: batch changes
element.style.cssText = 'width: 100px; height: 100px; background-color: red;';
// Or use CSS classes
element.className = 'optimized-styles';
}
Virtual Scrolling #
// Virtual scrolling implementation for large lists
class VirtualList {
constructor(container, itemHeight, totalItems) {
this.container = container;
this.itemHeight = itemHeight;
this.totalItems = totalItems;
this.visibleItems = Math.ceil(container.clientHeight / itemHeight) + 1;
this.startIndex = 0;
this.setupScrolling();
this.render();
}
setupScrolling() {
// Create spacers for total height
this.topSpacer = document.createElement('div');
this.bottomSpacer = document.createElement('div');
this.content = document.createElement('div');
this.container.appendChild(this.topSpacer);
this.container.appendChild(this.content);
this.container.appendChild(this.bottomSpacer);
this.container.addEventListener('scroll', () => {
this.handleScroll();
});
}
handleScroll() {
const scrollTop = this.container.scrollTop;
const newStartIndex = Math.floor(scrollTop / this.itemHeight);
if (newStartIndex !== this.startIndex) {
this.startIndex = newStartIndex;
this.render();
}
}
render() {
const endIndex = Math.min(
this.startIndex + this.visibleItems,
this.totalItems
);
// Update spacers
this.topSpacer.style.height = `${this.startIndex * this.itemHeight}px`;
this.bottomSpacer.style.height = `${(this.totalItems - endIndex) * this.itemHeight}px`;
// Render visible items
this.content.innerHTML = '';
for (let i = this.startIndex; i < endIndex; i++) {
const item = this.createItem(i);
this.content.appendChild(item);
}
}
createItem(index) {
const item = document.createElement('div');
item.style.height = `${this.itemHeight}px`;
item.textContent = `Item ${index}`;
return item;
}
}
3. Efficient Algorithms and Data Structures #
Optimizing Loops #
// Slow array operations
function slowArrayOperations(arr) {
let result = [];
// Multiple passes through array
const filtered = arr.filter(item => item > 5);
const mapped = filtered.map(item => item * 2);
const reduced = mapped.reduce((sum, item) => sum + item, 0);
return reduced;
}
// Optimized single pass
function optimizedArrayOperations(arr) {
return arr.reduce((sum, item) => {
if (item > 5) {
return sum + (item * 2);
}
return sum;
}, 0);
}
// Efficient object lookups
function efficientLookups() {
// Use Map for frequent lookups
const userMap = new Map();
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 3, name: 'Charlie' }
];
// Build lookup map once
users.forEach(user => {
userMap.set(user.id, user);
});
// O(1) lookup instead of O(n) with find()
function getUser(id) {
return userMap.get(id);
}
}
// Memoization for expensive computations
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;
};
}
// Example usage
const expensiveCalculation = memoize((n) => {
console.log('Computing for', n);
return n * n * n;
});
console.log(expensiveCalculation(5)); // Computes
console.log(expensiveCalculation(5)); // Uses cache
Debouncing and Throttling #
// Debounce - delay execution until after wait time
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Throttle - limit execution frequency
function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// Usage examples
const searchInput = document.getElementById('search');
const scrollHandler = throttle(() => {
console.log('Scroll event');
}, 100);
const searchHandler = debounce((query) => {
console.log('Searching for:', query);
}, 300);
window.addEventListener('scroll', scrollHandler);
searchInput.addEventListener('input', (e) => {
searchHandler(e.target.value);
});
4. Asynchronous Optimization #
Efficient Promise Handling #
// Inefficient sequential operations
async function inefficientAsync() {
const result1 = await fetch('/api/data1');
const result2 = await fetch('/api/data2');
const result3 = await fetch('/api/data3');
return [result1, result2, result3];
}
// Efficient concurrent operations
async function efficientAsync() {
const [result1, result2, result3] = await Promise.all([
fetch('/api/data1'),
fetch('/api/data2'),
fetch('/api/data3')
]);
return [result1, result2, result3];
}
// Controlled concurrency
async function controlledConcurrency(urls, maxConcurrent = 3) {
const results = [];
for (let i = 0; i < urls.length; i += maxConcurrent) {
const batch = urls.slice(i, i + maxConcurrent);
const batchResults = await Promise.all(
batch.map(url => fetch(url))
);
results.push(...batchResults);
}
return results;
}
// Request deduplication
class RequestDeduplicator {
constructor() {
this.pendingRequests = new Map();
}
async fetch(url) {
if (this.pendingRequests.has(url)) {
return this.pendingRequests.get(url);
}
const promise = fetch(url).then(response => {
this.pendingRequests.delete(url);
return response;
});
this.pendingRequests.set(url, promise);
return promise;
}
}
Web Workers for Heavy Computations #
// Main thread (main.js)
class WorkerPool {
constructor(workerScript, poolSize = 4) {
this.workers = [];
this.queue = [];
this.workerIndex = 0;
for (let i = 0; i < poolSize; i++) {
this.workers.push(new Worker(workerScript));
}
}
async execute(data) {
return new Promise((resolve, reject) => {
const worker = this.workers[this.workerIndex];
this.workerIndex = (this.workerIndex + 1) % this.workers.length;
const handleMessage = (event) => {
worker.removeEventListener('message', handleMessage);
worker.removeEventListener('error', handleError);
resolve(event.data);
};
const handleError = (error) => {
worker.removeEventListener('message', handleMessage);
worker.removeEventListener('error', handleError);
reject(error);
};
worker.addEventListener('message', handleMessage);
worker.addEventListener('error', handleError);
worker.postMessage(data);
});
}
terminate() {
this.workers.forEach(worker => worker.terminate());
}
}
// Usage
const workerPool = new WorkerPool('heavy-computation-worker.js');
async function processLargeDataset(dataset) {
try {
const results = await Promise.all(
dataset.map(chunk => workerPool.execute(chunk))
);
return results;
} catch (error) {
console.error('Worker error:', error);
}
}
// Worker script (heavy-computation-worker.js)
self.addEventListener('message', (event) => {
const data = event.data;
// Perform heavy computation
const result = performHeavyComputation(data);
// Send result back to main thread
self.postMessage(result);
});
function performHeavyComputation(data) {
// Simulate heavy computation
let result = 0;
for (let i = 0; i < data.length; i++) {
result += Math.sqrt(data[i]) * Math.sin(data[i]);
}
return result;
}
5. Code Splitting and Lazy Loading #
Dynamic Imports #
// Lazy load modules
async function loadModule(moduleName) {
try {
const module = await import(`./modules/${moduleName}.js`);
return module.default;
} catch (error) {
console.error('Failed to load module:', moduleName, error);
throw error;
}
}
// Component lazy loading
class ComponentLoader {
constructor() {
this.cache = new Map();
}
async loadComponent(componentName) {
if (this.cache.has(componentName)) {
return this.cache.get(componentName);
}
const component = await import(`./components/${componentName}.js`);
this.cache.set(componentName, component);
return component;
}
}
// Route-based code splitting
class Router {
constructor() {
this.routes = new Map();
}
addRoute(path, loader) {
this.routes.set(path, loader);
}
async navigate(path) {
const loader = this.routes.get(path);
if (loader) {
const module = await loader();
// Render component
module.render();
}
}
}
// Usage
const router = new Router();
router.addRoute('/dashboard', () => import('./pages/Dashboard.js'));
router.addRoute('/profile', () => import('./pages/Profile.js'));
Image and Resource Lazy Loading #
// Intersection Observer for lazy loading
class LazyLoader {
constructor() {
this.observer = new IntersectionObserver(
(entries) => this.handleIntersection(entries),
{ rootMargin: '50px' }
);
}
observe(element) {
this.observer.observe(element);
}
handleIntersection(entries) {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.loadResource(entry.target);
this.observer.unobserve(entry.target);
}
});
}
loadResource(element) {
if (element.dataset.src) {
element.src = element.dataset.src;
}
if (element.dataset.module) {
import(element.dataset.module).then(module => {
element.textContent = module.content;
});
}
}
}
// Usage
const lazyLoader = new LazyLoader();
document.querySelectorAll('[data-lazy]').forEach(element => {
lazyLoader.observe(element);
});
6. Performance Monitoring #
Performance Metrics #
// Performance monitoring class
class PerformanceMonitor {
constructor() {
this.metrics = new Map();
this.observers = [];
}
// Mark performance points
mark(name) {
performance.mark(name);
}
// Measure time between marks
measure(name, startMark, endMark) {
performance.measure(name, startMark, endMark);
const measure = performance.getEntriesByName(name)[0];
this.metrics.set(name, measure.duration);
return measure.duration;
}
// Time function execution
timeFunction(fn, name) {
const startTime = performance.now();
const result = fn();
const endTime = performance.now();
const duration = endTime - startTime;
this.metrics.set(name, duration);
console.log(`${name} took ${duration.toFixed(2)}ms`);
return result;
}
// Monitor frame rate
monitorFPS() {
let lastTime = performance.now();
let frameCount = 0;
const measureFPS = (currentTime) => {
frameCount++;
if (currentTime - lastTime >= 1000) {
const fps = frameCount / ((currentTime - lastTime) / 1000);
console.log(`FPS: ${fps.toFixed(2)}`);
frameCount = 0;
lastTime = currentTime;
}
requestAnimationFrame(measureFPS);
};
requestAnimationFrame(measureFPS);
}
// Monitor memory usage
monitorMemory() {
if (performance.memory) {
const memory = performance.memory;
console.log({
used: `${(memory.usedJSHeapSize / 1024 / 1024).toFixed(2)} MB`,
total: `${(memory.totalJSHeapSize / 1024 / 1024).toFixed(2)} MB`,
limit: `${(memory.jsHeapSizeLimit / 1024 / 1024).toFixed(2)} MB`
});
}
}
// Long task detection
detectLongTasks() {
if ('PerformanceObserver' in window) {
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach(entry => {
if (entry.duration > 50) {
console.warn(`Long task detected: ${entry.duration.toFixed(2)}ms`);
}
});
});
observer.observe({ entryTypes: ['longtask'] });
}
}
}
// Usage
const monitor = new PerformanceMonitor();
// Time a function
const result = monitor.timeFunction(() => {
return heavyComputation();
}, 'heavy-computation');
// Mark and measure
monitor.mark('start-api-call');
fetch('/api/data').then(() => {
monitor.mark('end-api-call');
monitor.measure('api-call-duration', 'start-api-call', 'end-api-call');
});
// Start monitoring
monitor.monitorFPS();
monitor.detectLongTasks();
Custom Performance Hooks #
// Performance timing hooks
class PerformanceHooks {
constructor() {
this.hooks = new Map();
}
// Register performance hook
register(name, callback) {
if (!this.hooks.has(name)) {
this.hooks.set(name, []);
}
this.hooks.get(name).push(callback);
}
// Trigger performance measurement
measure(name, duration, metadata = {}) {
const hooks = this.hooks.get(name) || [];
hooks.forEach(callback => {
callback(duration, metadata);
});
}
// Create performance decorator
createDecorator(name) {
return (target, propertyKey, descriptor) => {
const originalMethod = descriptor.value;
descriptor.value = function(...args) {
const start = performance.now();
const result = originalMethod.apply(this, args);
const end = performance.now();
this.measure(name, end - start, {
method: propertyKey,
args: args.length
});
return result;
};
};
}
}
// Usage
const perfHooks = new PerformanceHooks();
perfHooks.register('api-call', (duration, metadata) => {
if (duration > 1000) {
console.warn(`Slow API call: ${duration.toFixed(2)}ms`);
}
});
perfHooks.register('render', (duration, metadata) => {
if (duration > 16) {
console.warn(`Slow render: ${duration.toFixed(2)}ms`);
}
});
7. Bundle Optimization #
Tree Shaking #
// Efficient imports to enable tree shaking
// Bad: imports entire library
import _ from 'lodash';
// Good: import only what you need
import { debounce, throttle } from 'lodash';
// Even better: import from specific modules
import debounce from 'lodash/debounce';
import throttle from 'lodash/throttle';
// Custom utility functions
export const utils = {
debounce,
throttle,
formatDate: (date) => date.toLocaleDateString(),
formatCurrency: (amount) => `$${amount.toFixed(2)}`
};
Resource Hints #
<!-- Preload critical resources -->
<link rel="preload" href="critical.js" as="script">
<link rel="preload" href="critical.css" as="style">
<link rel="preload" href="hero-image.jpg" as="image">
<!-- Prefetch resources for next navigation -->
<link rel="prefetch" href="next-page.js">
<link rel="prefetch" href="next-page.css">
<!-- Preconnect to external domains -->
<link rel="preconnect" href="https://api.example.com">
<link rel="preconnect" href="https://fonts.googleapis.com">
8. Testing Performance #
Benchmarking #
// Benchmark suite
class BenchmarkSuite {
constructor() {
this.tests = [];
}
add(name, fn) {
this.tests.push({ name, fn });
}
async run() {
const results = [];
for (const test of this.tests) {
const iterations = 1000;
const times = [];
// Warmup
for (let i = 0; i < 100; i++) {
test.fn();
}
// Measure
for (let i = 0; i < iterations; i++) {
const start = performance.now();
test.fn();
const end = performance.now();
times.push(end - start);
}
const avg = times.reduce((a, b) => a + b) / times.length;
const min = Math.min(...times);
const max = Math.max(...times);
results.push({
name: test.name,
avg: avg.toFixed(4),
min: min.toFixed(4),
max: max.toFixed(4)
});
}
console.table(results);
return results;
}
}
// Usage
const suite = new BenchmarkSuite();
suite.add('Array.map', () => {
const arr = Array.from({ length: 1000 }, (_, i) => i);
return arr.map(x => x * 2);
});
suite.add('For loop', () => {
const arr = Array.from({ length: 1000 }, (_, i) => i);
const result = [];
for (let i = 0; i < arr.length; i++) {
result.push(arr[i] * 2);
}
return result;
});
suite.run();
Summary #
JavaScript performance optimization involves:
- Memory Management - Prevent leaks and optimize garbage collection
- DOM Optimization - Minimize reflows and batch operations
- Efficient Algorithms - Use appropriate data structures and algorithms
- Async Optimization - Handle promises efficiently and use Web Workers
- Code Splitting - Load code on demand
- Performance Monitoring - Measure and track performance metrics
- Bundle Optimization - Minimize bundle size and enable tree shaking
- Testing - Benchmark and profile your code
Regular profiling and monitoring help identify bottlenecks and ensure your applications remain fast and responsive.
Next Steps #
Related Tutorials
Asynchronous JavaScript - Promises, Async/Await, and Fetch
Master asynchronous programming in JavaScript with promises, async/await, and the Fetch API. Learn to handle API calls, timers, and concurrent operations.
Best Way to Learn JavaScript: Common Questions Answered
Find answers to frequently asked questions about the best way to learn JavaScript, including timelines, resources, and effective study methods.
Last updated: Jan 11, 2025
DOM Manipulation with JavaScript - Complete Guide
Master DOM manipulation in JavaScript. Learn to select elements, modify content, handle events, and create dynamic web pages with practical examples.
ES6+ Features Every JavaScript Developer Should Know
Master modern JavaScript with ES6+ features including arrow functions, destructuring, modules, async/await, and more essential syntax improvements.