JavaScript Performance Optimization: Speed Up Your Applications
Master JavaScript performance optimization techniques. Learn profiling, memory management, code optimization, bundling strategies, and performance monitoring.
Performance optimization is crucial for creating fast, responsive JavaScript applications. This comprehensive guide covers profiling techniques, memory management, code optimization strategies, and performance monitoring to help you build lightning-fast applications.
Performance Profiling and Measurement
Understanding where performance bottlenecks occur is the first step in optimization.
Performance Measurement Tools
// Performance measurement utilities
class PerformanceProfiler {
constructor() {
this.measurements = new Map();
this.markers = new Map();
}
// Mark the start of a performance measurement
mark(name) {
const markName = `${name}-start`;
performance.mark(markName);
this.markers.set(name, Date.now());
return this;
}
// Measure from mark to now
measure(name) {
const startMark = `${name}-start`;
const endMark = `${name}-end`;
performance.mark(endMark);
try {
performance.measure(name, startMark, endMark);
const measure = performance.getEntriesByName(name, 'measure')[0];
this.measurements.set(name, {
duration: measure.duration,
startTime: measure.startTime,
endTime: measure.startTime + measure.duration,
});
// Clean up marks
performance.clearMarks(startMark);
performance.clearMarks(endMark);
performance.clearMeasures(name);
return measure.duration;
} catch (error) {
console.warn(`Failed to measure ${name}:`, error);
return null;
}
}
// Time a function execution
async timeFunction(fn, name = 'function') {
this.mark(name);
try {
const result = await fn();
const duration = this.measure(name);
return {
result,
duration,
name,
};
} catch (error) {
this.measure(name);
throw error;
}
}
// Time multiple iterations of a function
async benchmark(fn, iterations = 1000, name = 'benchmark') {
const results = [];
for (let i = 0; i < iterations; i++) {
const timing = await this.timeFunction(fn, `${name}-${i}`);
results.push(timing.duration);
}
const sorted = results.sort((a, b) => a - b);
const sum = results.reduce((acc, val) => acc + val, 0);
return {
iterations,
min: sorted[0],
max: sorted[sorted.length - 1],
mean: sum / iterations,
median: sorted[Math.floor(sorted.length / 2)],
p95: sorted[Math.floor(sorted.length * 0.95)],
p99: sorted[Math.floor(sorted.length * 0.99)],
results,
};
}
// Memory usage measurement
measureMemory() {
if ('memory' in performance) {
return {
usedJSHeapSize: performance.memory.usedJSHeapSize,
totalJSHeapSize: performance.memory.totalJSHeapSize,
jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
usedPercentage:
(performance.memory.usedJSHeapSize /
performance.memory.jsHeapSizeLimit) *
100,
};
}
return null;
}
// Resource timing analysis
analyzeResourceTiming() {
const resources = performance.getEntriesByType('resource');
const analysis = {
totalResources: resources.length,
totalSize: 0,
totalDuration: 0,
byType: {},
slowest: [],
largest: [],
};
resources.forEach((resource) => {
const type = this.getResourceType(resource.name);
const duration = resource.responseEnd - resource.startTime;
const size = resource.transferSize || 0;
if (!analysis.byType[type]) {
analysis.byType[type] = {
count: 0,
totalSize: 0,
totalDuration: 0,
averageDuration: 0,
};
}
analysis.byType[type].count++;
analysis.byType[type].totalSize += size;
analysis.byType[type].totalDuration += duration;
analysis.byType[type].averageDuration =
analysis.byType[type].totalDuration / analysis.byType[type].count;
analysis.totalSize += size;
analysis.totalDuration += duration;
analysis.slowest.push({ name: resource.name, duration, type });
analysis.largest.push({ name: resource.name, size, type });
});
// Sort for top lists
analysis.slowest.sort((a, b) => b.duration - a.duration).splice(10);
analysis.largest.sort((a, b) => b.size - a.size).splice(10);
return analysis;
}
getResourceType(url) {
if (url.match(/\.(js|mjs)(\?|$)/)) return 'javascript';
if (url.match(/\.(css)(\?|$)/)) return 'css';
if (url.match(/\.(png|jpg|jpeg|gif|svg|webp)(\?|$)/)) return 'image';
if (url.match(/\.(woff|woff2|ttf|eot)(\?|$)/)) return 'font';
if (url.match(/\.(mp4|webm|ogg|mp3|wav)(\?|$)/)) return 'media';
return 'other';
}
// Generate performance report
generateReport() {
return {
measurements: Object.fromEntries(this.measurements),
memory: this.measureMemory(),
resources: this.analyzeResourceTiming(),
timestamp: new Date().toISOString(),
};
}
// Clear all measurements
clear() {
this.measurements.clear();
this.markers.clear();
performance.clearMarks();
performance.clearMeasures();
}
}
// Usage examples
const profiler = new PerformanceProfiler();
// Basic timing
profiler.mark('data-processing');
// ... do some work
const duration = profiler.measure('data-processing');
console.log(`Processing took ${duration}ms`);
// Function timing
async function expensiveOperation() {
return new Promise((resolve) => setTimeout(resolve, 100));
}
const result = await profiler.timeFunction(expensiveOperation, 'expensive-op');
console.log(`Operation completed in ${result.duration}ms`);
// Benchmarking
const benchmark = await profiler.benchmark(
() => {
return Array.from({ length: 1000 }, (_, i) => i * 2);
},
100,
'array-creation'
);
console.log('Benchmark results:', benchmark);
Core Web Vitals Monitoring
// Core Web Vitals and performance metrics monitoring
class WebVitalsMonitor {
constructor() {
this.metrics = new Map();
this.observers = new Map();
this.thresholds = {
LCP: { good: 2500, poor: 4000 },
FID: { good: 100, poor: 300 },
CLS: { good: 0.1, poor: 0.25 },
FCP: { good: 1800, poor: 3000 },
TTFB: { good: 800, poor: 1800 },
};
this.init();
}
init() {
this.observeLCP();
this.observeFID();
this.observeCLS();
this.observeFCP();
this.observeTTFB();
this.observeNavigationTiming();
}
// Largest Contentful Paint
observeLCP() {
if ('PerformanceObserver' in window) {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
this.recordMetric('LCP', {
value: lastEntry.startTime,
element: lastEntry.element,
timestamp: Date.now(),
rating: this.getRating('LCP', lastEntry.startTime),
});
});
observer.observe({ entryTypes: ['largest-contentful-paint'] });
this.observers.set('LCP', observer);
}
}
// First Input Delay
observeFID() {
if ('PerformanceObserver' in window) {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach((entry) => {
this.recordMetric('FID', {
value: entry.processingStart - entry.startTime,
eventType: entry.name,
timestamp: Date.now(),
rating: this.getRating(
'FID',
entry.processingStart - entry.startTime
),
});
});
});
observer.observe({ entryTypes: ['first-input'] });
this.observers.set('FID', observer);
}
}
// Cumulative Layout Shift
observeCLS() {
if ('PerformanceObserver' in window) {
let clsValue = 0;
let sessionValue = 0;
let sessionEntries = [];
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach((entry) => {
if (!entry.hadRecentInput) {
const firstSessionEntry = sessionEntries[0];
const lastSessionEntry = sessionEntries[sessionEntries.length - 1];
if (
sessionValue &&
entry.startTime - lastSessionEntry.startTime < 1000 &&
entry.startTime - firstSessionEntry.startTime < 5000
) {
sessionValue += entry.value;
sessionEntries.push(entry);
} else {
sessionValue = entry.value;
sessionEntries = [entry];
}
if (sessionValue > clsValue) {
clsValue = sessionValue;
this.recordMetric('CLS', {
value: clsValue,
entries: sessionEntries.map((e) => ({
value: e.value,
sources: e.sources,
})),
timestamp: Date.now(),
rating: this.getRating('CLS', clsValue),
});
}
}
});
});
observer.observe({ entryTypes: ['layout-shift'] });
this.observers.set('CLS', observer);
}
}
// First Contentful Paint
observeFCP() {
if ('PerformanceObserver' in window) {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const fcpEntry = entries.find(
(entry) => entry.name === 'first-contentful-paint'
);
if (fcpEntry) {
this.recordMetric('FCP', {
value: fcpEntry.startTime,
timestamp: Date.now(),
rating: this.getRating('FCP', fcpEntry.startTime),
});
}
});
observer.observe({ entryTypes: ['paint'] });
this.observers.set('FCP', observer);
}
}
// Time to First Byte
observeTTFB() {
if ('PerformanceObserver' in window) {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach((entry) => {
if (entry.entryType === 'navigation') {
const ttfb = entry.responseStart - entry.requestStart;
this.recordMetric('TTFB', {
value: ttfb,
timestamp: Date.now(),
rating: this.getRating('TTFB', ttfb),
});
}
});
});
observer.observe({ entryTypes: ['navigation'] });
this.observers.set('TTFB', observer);
}
}
// Navigation timing
observeNavigationTiming() {
window.addEventListener('load', () => {
const navigation = performance.getEntriesByType('navigation')[0];
if (navigation) {
this.recordMetric('Navigation', {
domContentLoaded:
navigation.domContentLoadedEventEnd -
navigation.domContentLoadedEventStart,
loadComplete: navigation.loadEventEnd - navigation.loadEventStart,
domInteractive: navigation.domInteractive - navigation.fetchStart,
domComplete: navigation.domComplete - navigation.fetchStart,
timestamp: Date.now(),
});
}
});
}
recordMetric(name, data) {
if (!this.metrics.has(name)) {
this.metrics.set(name, []);
}
this.metrics.get(name).push(data);
// Dispatch custom event
window.dispatchEvent(
new CustomEvent('webvital', {
detail: { name, data },
})
);
}
getRating(metric, value) {
const thresholds = this.thresholds[metric];
if (!thresholds) return 'unknown';
if (value <= thresholds.good) return 'good';
if (value <= thresholds.poor) return 'needs-improvement';
return 'poor';
}
getMetrics() {
const result = {};
this.metrics.forEach((values, name) => {
const latest = values[values.length - 1];
result[name] = {
current: latest,
history: values,
count: values.length,
};
});
return result;
}
// Get performance score
getPerformanceScore() {
const metrics = this.getMetrics();
const scores = {};
let totalScore = 0;
let metricCount = 0;
Object.entries(metrics).forEach(([name, data]) => {
if (this.thresholds[name] && data.current) {
const rating = data.current.rating;
let score = 0;
if (rating === 'good') score = 100;
else if (rating === 'needs-improvement') score = 50;
else score = 0;
scores[name] = score;
totalScore += score;
metricCount++;
}
});
return {
overall: metricCount > 0 ? Math.round(totalScore / metricCount) : 0,
individual: scores,
};
}
// Disconnect all observers
disconnect() {
this.observers.forEach((observer) => {
observer.disconnect();
});
this.observers.clear();
}
}
// Usage
const vitalsMonitor = new WebVitalsMonitor();
// Listen for metric updates
window.addEventListener('webvital', (event) => {
console.log('Web Vital recorded:', event.detail);
});
// Get current metrics
setTimeout(() => {
const metrics = vitalsMonitor.getMetrics();
const score = vitalsMonitor.getPerformanceScore();
console.log('Current metrics:', metrics);
console.log('Performance score:', score);
}, 5000);
Memory Management and Optimization
Memory Leak Detection and Prevention
// Memory management utilities
class MemoryManager {
constructor() {
this.references = new WeakMap();
this.eventListeners = new Map();
this.intervals = new Set();
this.timeouts = new Set();
this.observers = new Set();
}
// Weak reference storage for objects
trackObject(obj, metadata = {}) {
this.references.set(obj, {
...metadata,
createdAt: Date.now(),
trackedAt: Date.now(),
});
return obj;
}
// Event listener management
addEventListener(element, event, handler, options) {
const listener = { element, event, handler, options };
this.eventListeners.set(handler, listener);
element.addEventListener(event, handler, options);
return () => this.removeEventListener(handler);
}
removeEventListener(handler) {
const listener = this.eventListeners.get(handler);
if (listener) {
listener.element.removeEventListener(
listener.event,
listener.handler,
listener.options
);
this.eventListeners.delete(handler);
}
}
// Timer management
setTimeout(callback, delay, ...args) {
const id = setTimeout(() => {
this.timeouts.delete(id);
callback(...args);
}, delay);
this.timeouts.add(id);
return id;
}
setInterval(callback, interval, ...args) {
const id = setInterval(callback, interval, ...args);
this.intervals.add(id);
return id;
}
clearTimeout(id) {
clearTimeout(id);
this.timeouts.delete(id);
}
clearInterval(id) {
clearInterval(id);
this.intervals.delete(id);
}
// Observer management
createObserver(observerClass, callback, options = {}) {
const observer = new observerClass(callback);
observer.observe(document.body, options);
this.observers.add(observer);
return observer;
}
// Cleanup all managed resources
cleanup() {
// Remove event listeners
this.eventListeners.forEach((listener, handler) => {
this.removeEventListener(handler);
});
// Clear timers
this.intervals.forEach((id) => clearInterval(id));
this.timeouts.forEach((id) => clearTimeout(id));
// Disconnect observers
this.observers.forEach((observer) => {
if (observer.disconnect) observer.disconnect();
});
this.eventListeners.clear();
this.intervals.clear();
this.timeouts.clear();
this.observers.clear();
}
// Memory usage estimation
estimateObjectSize(obj, seen = new WeakSet()) {
if (obj === null || typeof obj !== 'object') {
return this.getPrimitiveSize(obj);
}
if (seen.has(obj)) {
return 0; // Circular reference
}
seen.add(obj);
let size = 0;
if (Array.isArray(obj)) {
size += 24; // Array overhead
for (const item of obj) {
size += 8; // Pointer size
size += this.estimateObjectSize(item, seen);
}
} else if (obj instanceof Date) {
size += 24;
} else if (obj instanceof RegExp) {
size += 24 + obj.source.length * 2;
} else {
size += 24; // Object overhead
for (const [key, value] of Object.entries(obj)) {
size += key.length * 2; // String key
size += 8; // Property overhead
size += this.estimateObjectSize(value, seen);
}
}
return size;
}
getPrimitiveSize(value) {
switch (typeof value) {
case 'string':
return value.length * 2; // UTF-16
case 'number':
return 8;
case 'boolean':
return 4;
case 'undefined':
case 'symbol':
return 8;
default:
return 8;
}
}
// Memory leak detection
detectMemoryLeaks() {
const report = {
eventListeners: this.eventListeners.size,
intervals: this.intervals.size,
timeouts: this.timeouts.size,
observers: this.observers.size,
detachedNodes: this.findDetachedNodes(),
largeDOMTrees: this.findLargeDOMTrees(),
};
return report;
}
findDetachedNodes() {
// This is a simplified detection - in practice, you'd use browser dev tools
const allElements = document.querySelectorAll('*');
const detached = [];
allElements.forEach((el) => {
if (
!document.body.contains(el) &&
el !== document.body &&
el !== document.documentElement
) {
detached.push({
tagName: el.tagName,
id: el.id,
className: el.className,
});
}
});
return detached;
}
findLargeDOMTrees(threshold = 1000) {
const large = [];
const countDescendants = (element) => {
return element.querySelectorAll('*').length;
};
document.querySelectorAll('*').forEach((el) => {
const count = countDescendants(el);
if (count > threshold) {
large.push({
element: el.tagName,
id: el.id,
className: el.className,
descendants: count,
});
}
});
return large.sort((a, b) => b.descendants - a.descendants).slice(0, 10);
}
}
// Object pool for reducing garbage collection
class ObjectPool {
constructor(createFn, resetFn, initialSize = 10) {
this.createFn = createFn;
this.resetFn = resetFn;
this.pool = [];
this.allocated = new Set();
// Pre-allocate objects
for (let i = 0; i < initialSize; i++) {
this.pool.push(this.createFn());
}
}
acquire() {
let obj;
if (this.pool.length > 0) {
obj = this.pool.pop();
} else {
obj = this.createFn();
}
this.allocated.add(obj);
return obj;
}
release(obj) {
if (this.allocated.has(obj)) {
this.allocated.delete(obj);
if (this.resetFn) {
this.resetFn(obj);
}
this.pool.push(obj);
}
}
releaseAll() {
this.allocated.forEach((obj) => this.release(obj));
}
getStats() {
return {
poolSize: this.pool.length,
allocated: this.allocated.size,
total: this.pool.length + this.allocated.size,
};
}
}
// Usage examples
const memoryManager = new MemoryManager();
// Managed event listeners
const cleanup = memoryManager.addEventListener(
document.getElementById('button'),
'click',
() => console.log('clicked')
);
// Object pool example
const vectorPool = new ObjectPool(
() => ({ x: 0, y: 0, z: 0 }),
(vec) => {
vec.x = 0;
vec.y = 0;
vec.z = 0;
}
);
const vector = vectorPool.acquire();
vector.x = 10;
vector.y = 20;
// ... use vector
vectorPool.release(vector);
// Memory leak detection
const leakReport = memoryManager.detectMemoryLeaks();
console.log('Memory leak report:', leakReport);
// Cleanup on page unload
window.addEventListener('beforeunload', () => {
memoryManager.cleanup();
vectorPool.releaseAll();
});
Code Optimization Techniques
Algorithm and Data Structure Optimizations
// Optimized data structures and algorithms
class OptimizedDataStructures {
// Fast lookup table using Map
static createLookupTable(array, keyFn) {
const table = new Map();
for (let i = 0; i < array.length; i++) {
const item = array[i];
const key = keyFn(item);
if (table.has(key)) {
// Handle collisions with arrays
const existing = table.get(key);
if (Array.isArray(existing)) {
existing.push(item);
} else {
table.set(key, [existing, item]);
}
} else {
table.set(key, item);
}
}
return table;
}
// Optimized array operations
static fastFilter(array, predicate) {
const result = [];
for (let i = 0; i < array.length; i++) {
if (predicate(array[i], i)) {
result.push(array[i]);
}
}
return result;
}
static fastMap(array, mapper) {
const result = new Array(array.length);
for (let i = 0; i < array.length; i++) {
result[i] = mapper(array[i], i);
}
return result;
}
static fastReduce(array, reducer, initialValue) {
let accumulator = initialValue;
for (let i = 0; i < array.length; i++) {
accumulator = reducer(accumulator, array[i], i);
}
return accumulator;
}
// Efficient array deduplication
static deduplicate(array) {
return [...new Set(array)];
}
static deduplicateBy(array, keyFn) {
const seen = new Set();
const result = [];
for (let i = 0; i < array.length; i++) {
const item = array[i];
const key = keyFn(item);
if (!seen.has(key)) {
seen.add(key);
result.push(item);
}
}
return result;
}
// Binary search for sorted arrays
static binarySearch(sortedArray, target, compareFn = (a, b) => a - b) {
let left = 0;
let right = sortedArray.length - 1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
const comparison = compareFn(sortedArray[mid], target);
if (comparison === 0) return mid;
if (comparison < 0) left = mid + 1;
else right = mid - 1;
}
return -1;
}
// Efficient string operations
static fastStringConcat(strings) {
// Use array join for better performance with many strings
if (strings.length > 10) {
return strings.join('');
}
// Use template literals for fewer strings
return strings.reduce((acc, str) => acc + str, '');
}
// Optimized object operations
static fastObjectClone(obj) {
// For simple objects, use spread operator
if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
const keys = Object.keys(obj);
if (keys.length < 10) {
return { ...obj };
}
}
// For complex objects, use JSON (if no functions/undefined values)
try {
return JSON.parse(JSON.stringify(obj));
} catch {
// Fallback to manual cloning
return this.deepClone(obj);
}
}
static deepClone(obj, seen = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj;
if (seen.has(obj)) return seen.get(obj);
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
if (obj instanceof Array) {
const clone = [];
seen.set(obj, clone);
for (let i = 0; i < obj.length; i++) {
clone[i] = this.deepClone(obj[i], seen);
}
return clone;
}
const clone = {};
seen.set(obj, clone);
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = this.deepClone(obj[key], seen);
}
}
return clone;
}
}
// Function memoization for expensive computations
class Memoizer {
constructor(options = {}) {
this.cache = new Map();
this.maxSize = options.maxSize || 1000;
this.ttl = options.ttl; // Time to live in milliseconds
this.accessOrder = []; // For LRU eviction
}
memoize(fn, keyGenerator = (...args) => JSON.stringify(args)) {
return (...args) => {
const key = keyGenerator(...args);
const now = Date.now();
// Check if cached result exists and is valid
if (this.cache.has(key)) {
const cached = this.cache.get(key);
if (!this.ttl || now - cached.timestamp < this.ttl) {
// Update access order for LRU
this.updateAccessOrder(key);
return cached.value;
} else {
// Expired cache entry
this.cache.delete(key);
this.removeFromAccessOrder(key);
}
}
// Compute new result
const result = fn(...args);
// Store in cache
this.cache.set(key, {
value: result,
timestamp: now,
});
this.updateAccessOrder(key);
// Evict if necessary
if (this.cache.size > this.maxSize) {
this.evictLRU();
}
return result;
};
}
updateAccessOrder(key) {
this.removeFromAccessOrder(key);
this.accessOrder.push(key);
}
removeFromAccessOrder(key) {
const index = this.accessOrder.indexOf(key);
if (index > -1) {
this.accessOrder.splice(index, 1);
}
}
evictLRU() {
if (this.accessOrder.length > 0) {
const lruKey = this.accessOrder.shift();
this.cache.delete(lruKey);
}
}
clear() {
this.cache.clear();
this.accessOrder = [];
}
getStats() {
return {
cacheSize: this.cache.size,
maxSize: this.maxSize,
hitRate: this.hitCount / (this.hitCount + this.missCount) || 0,
};
}
}
// Debouncing and throttling for performance
class RateLimiter {
static debounce(fn, delay = 300) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
static throttle(fn, limit = 100) {
let inThrottle;
return function (...args) {
if (!inThrottle) {
fn.apply(this, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
}
static rafThrottle(fn) {
let rafId;
let lastArgs;
return function (...args) {
lastArgs = args;
if (!rafId) {
rafId = requestAnimationFrame(() => {
fn.apply(this, lastArgs);
rafId = null;
});
}
};
}
static idleCallback(fn, options = {}) {
if ('requestIdleCallback' in window) {
return requestIdleCallback(fn, options);
} else {
// Fallback for browsers without requestIdleCallback
return setTimeout(fn, 0);
}
}
}
// Usage examples
const data = [
{ id: 1, name: 'Alice', category: 'A' },
{ id: 2, name: 'Bob', category: 'B' },
{ id: 3, name: 'Charlie', category: 'A' },
];
// Fast lookup table
const lookupTable = OptimizedDataStructures.createLookupTable(
data,
(item) => item.category
);
console.log(lookupTable.get('A')); // Items in category A
// Memoized expensive function
const memoizer = new Memoizer({ maxSize: 100, ttl: 60000 });
const expensiveFunction = (n) => {
// Simulate expensive computation
let result = 0;
for (let i = 0; i < n * 1000; i++) {
result += Math.sqrt(i);
}
return result;
};
const memoizedFunction = memoizer.memoize(expensiveFunction);
console.time('First call');
console.log(memoizedFunction(1000)); // Slow
console.timeEnd('First call');
console.time('Second call');
console.log(memoizedFunction(1000)); // Fast (cached)
console.timeEnd('Second call');
// Debounced search
const debouncedSearch = RateLimiter.debounce((query) => {
console.log('Searching for:', query);
// Perform search
}, 300);
Bundle Optimization and Code Splitting
Dynamic Imports and Lazy Loading
// Dynamic import utilities for code splitting
class ModuleLoader {
constructor() {
this.cache = new Map();
this.loading = new Map();
this.retryAttempts = 3;
this.retryDelay = 1000;
}
// Load module with caching and error handling
async loadModule(modulePath, options = {}) {
// Return cached module if available
if (this.cache.has(modulePath)) {
return this.cache.get(modulePath);
}
// Return existing loading promise if in progress
if (this.loading.has(modulePath)) {
return this.loading.get(modulePath);
}
// Start loading
const loadingPromise = this.loadWithRetry(modulePath, options);
this.loading.set(modulePath, loadingPromise);
try {
const module = await loadingPromise;
this.cache.set(modulePath, module);
this.loading.delete(modulePath);
return module;
} catch (error) {
this.loading.delete(modulePath);
throw error;
}
}
async loadWithRetry(modulePath, options, attempt = 1) {
try {
const module = await import(modulePath);
return module;
} catch (error) {
if (attempt < this.retryAttempts) {
await this.delay(this.retryDelay * attempt);
return this.loadWithRetry(modulePath, options, attempt + 1);
}
throw error;
}
}
// Preload modules for better performance
async preloadModule(modulePath) {
if (this.cache.has(modulePath) || this.loading.has(modulePath)) {
return;
}
// Use link rel="modulepreload" if available
if ('HTMLLinkElement' in window && 'relList' in HTMLLinkElement.prototype) {
const link = document.createElement('link');
link.rel = 'modulepreload';
link.href = modulePath;
document.head.appendChild(link);
}
// Also start actual loading
this.loadModule(modulePath).catch(console.error);
}
// Load multiple modules in parallel
async loadModules(modulePaths) {
const promises = modulePaths.map((path) => this.loadModule(path));
return Promise.all(promises);
}
// Load modules with priority
async loadModulesWithPriority(moduleConfigs) {
const highPriority = [];
const lowPriority = [];
moduleConfigs.forEach((config) => {
if (config.priority === 'high') {
highPriority.push(this.loadModule(config.path));
} else {
lowPriority.push(this.loadModule(config.path));
}
});
// Load high priority first
const highPriorityResults = await Promise.all(highPriority);
// Then load low priority
const lowPriorityResults = await Promise.all(lowPriority);
return [...highPriorityResults, ...lowPriorityResults];
}
delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// Clear cache
clearCache(modulePath) {
if (modulePath) {
this.cache.delete(modulePath);
} else {
this.cache.clear();
}
}
// Get loading statistics
getStats() {
return {
cached: this.cache.size,
loading: this.loading.size,
cachedModules: Array.from(this.cache.keys()),
};
}
}
// Component lazy loading with intersection observer
class LazyComponentLoader {
constructor(options = {}) {
this.threshold = options.threshold || 0.1;
this.rootMargin = options.rootMargin || '50px';
this.components = new Map();
this.moduleLoader = new ModuleLoader();
this.observer = new IntersectionObserver(
this.handleIntersection.bind(this),
{
threshold: this.threshold,
rootMargin: this.rootMargin,
}
);
}
// Register a component for lazy loading
register(element, componentPath, props = {}) {
this.components.set(element, {
path: componentPath,
props,
loaded: false,
});
this.observer.observe(element);
return {
unregister: () => {
this.unregister(element);
},
};
}
unregister(element) {
this.observer.unobserve(element);
this.components.delete(element);
}
async handleIntersection(entries) {
for (const entry of entries) {
if (entry.isIntersecting) {
const element = entry.target;
const config = this.components.get(element);
if (config && !config.loaded) {
config.loaded = true;
this.observer.unobserve(element);
try {
await this.loadComponent(element, config);
} catch (error) {
console.error('Failed to load component:', error);
this.showErrorState(element, error);
}
}
}
}
}
async loadComponent(element, config) {
// Show loading state
this.showLoadingState(element);
try {
const module = await this.moduleLoader.loadModule(config.path);
const Component = module.default || module;
// Initialize component
if (typeof Component === 'function') {
const instance = new Component(element, config.props);
if (instance.render) {
await instance.render();
}
} else if (Component.render) {
await Component.render(element, config.props);
}
this.hideLoadingState(element);
} catch (error) {
this.hideLoadingState(element);
throw error;
}
}
showLoadingState(element) {
element.classList.add('loading');
element.innerHTML = '<div class="lazy-loading">Loading...</div>';
}
hideLoadingState(element) {
element.classList.remove('loading');
const loader = element.querySelector('.lazy-loading');
if (loader) {
loader.remove();
}
}
showErrorState(element, error) {
element.classList.add('error');
element.innerHTML = `<div class="lazy-error">Failed to load component: ${error.message}</div>`;
}
// Preload components that will likely be needed
async preloadComponents(paths) {
const promises = paths.map((path) => this.moduleLoader.preloadModule(path));
return Promise.all(promises);
}
disconnect() {
this.observer.disconnect();
this.components.clear();
}
}
// Image lazy loading with progressive enhancement
class LazyImageLoader {
constructor(options = {}) {
this.threshold = options.threshold || 0.1;
this.rootMargin = options.rootMargin || '50px';
this.quality = options.quality || 'medium';
this.observer = new IntersectionObserver(
this.handleIntersection.bind(this),
{
threshold: this.threshold,
rootMargin: this.rootMargin,
}
);
}
observe(img) {
this.observer.observe(img);
}
unobserve(img) {
this.observer.unobserve(img);
}
async handleIntersection(entries) {
for (const entry of entries) {
if (entry.isIntersecting) {
const img = entry.target;
this.observer.unobserve(img);
try {
await this.loadImage(img);
} catch (error) {
console.error('Failed to load image:', error);
this.showErrorState(img);
}
}
}
}
async loadImage(img) {
const src = img.dataset.src;
if (!src) return;
// Show loading state
img.classList.add('loading');
// Create new image to preload
const newImg = new Image();
return new Promise((resolve, reject) => {
newImg.onload = () => {
img.src = src;
img.classList.remove('loading');
img.classList.add('loaded');
resolve();
};
newImg.onerror = () => {
img.classList.remove('loading');
reject(new Error('Failed to load image'));
};
// Support for responsive images
if (img.dataset.srcset) {
newImg.srcset = img.dataset.srcset;
}
newImg.src = src;
});
}
showErrorState(img) {
img.classList.add('error');
img.alt = 'Failed to load image';
}
// Batch process images
observeAll(selector = 'img[data-src]') {
const images = document.querySelectorAll(selector);
images.forEach((img) => this.observe(img));
return images.length;
}
disconnect() {
this.observer.disconnect();
}
}
// Usage examples
const moduleLoader = new ModuleLoader();
const lazyLoader = new LazyComponentLoader();
const imageLoader = new LazyImageLoader();
// Load a module dynamically
async function loadChart() {
try {
const chartModule = await moduleLoader.loadModule('./chart-component.js');
const chart = new chartModule.Chart({
type: 'line',
data: [1, 2, 3, 4, 5],
});
chart.render();
} catch (error) {
console.error('Failed to load chart:', error);
}
}
// Register lazy components
document.querySelectorAll('[data-component]').forEach((element) => {
const componentPath = element.dataset.component;
lazyLoader.register(element, componentPath);
});
// Setup lazy image loading
imageLoader.observeAll();
// Preload critical modules
moduleLoader.preloadModule('./critical-component.js');
// Cleanup on page unload
window.addEventListener('beforeunload', () => {
lazyLoader.disconnect();
imageLoader.disconnect();
});
Conclusion
JavaScript performance optimization is an ongoing process that requires understanding your application's specific bottlenecks and user requirements. Start with measurement and profiling to identify actual performance issues, then apply appropriate optimization techniques. Focus on the most impactful optimizations first: reducing bundle sizes, eliminating unnecessary re-renders, optimizing critical rendering paths, and implementing efficient caching strategies. Remember that premature optimization can lead to complex code without meaningful benefits, so always measure the impact of your optimizations.