HTML & JS Topic
JavaScript Performance Optimization
1. DOM Performance
Minimize DOM Access
javascript
// BAD: Multiple DOM queries in loop
for (let i = 0; i < 100; i++) {
document.querySelector('.item').textContent = i; // Queries DOM 100 times!
}
// GOOD: Cache DOM reference
const item = document.querySelector('.item'); // Query once
for (let i = 0; i < 100; i++) {
item.textContent = i; // Updates cached reference
}
// BAD: Reading layout properties causes reflows
function resizeElements() {
const elements = document.querySelectorAll('.box');
elements.forEach(el => {
const width = el.offsetWidth; // Forces layout
el.style.width = (width + 10) + 'px'; // Causes reflow
});
}
// GOOD: Batch read/write operations
function resizeElementsOptimized() {
const elements = document.querySelectorAll('.box');
// 1. Read all properties first
const widths = Array.from(elements).map(el => el.offsetWidth);
// 2. Write all changes (in single reflow)
elements.forEach((el, i) => {
el.style.width = (widths[i] + 10) + 'px';
});
}
Document Fragments for Bulk Updates
javascript
// BAD: Appending directly to DOM in loop
function addItemsDirect(items) {
const container = document.getElementById('container');
items.forEach(item => {
const div = document.createElement('div');
div.textContent = item;
container.appendChild(div); // Reflow each iteration
});
}
// GOOD: Use DocumentFragment
function addItemsOptimized(items) {
const container = document.getElementById('container');
const fragment = document.createDocumentFragment(); // Lightweight container
items.forEach(item => {
const div = document.createElement('div');
div.textContent = item;
fragment.appendChild(div); // No reflow here
});
container.appendChild(fragment); // Single reflow
}
// BETTER: Use innerHTML with string building
function addItemsBest(items) {
const container = document.getElementById('container');
let html = '';
items.forEach(item => {
html += `<div>${item}</div>`;
});
container.innerHTML = html; // Single reflow
}
Virtual DOM Pattern
javascript
// Simple Virtual DOM implementation
class VirtualDOM {
constructor() {
this.cache = new Map();
}
// Store element state
updateElement(id, newState) {
const oldState = this.cache.get(id);
const differences = this.diff(oldState, newState);
if (differences.length > 0) {
this.applyChanges(id, differences);
this.cache.set(id, newState);
}
}
// Find differences
diff(oldState, newState) {
const changes = [];
if (!oldState) {
return ['full-update']; // First render
}
// Compare properties
for (const key in newState) {
if (oldState[key] !== newState[key]) {
changes.push({ key, value: newState[key] });
}
}
return changes;
}
// Apply minimal changes
applyChanges(elementId, changes) {
const element = document.getElementById(elementId);
changes.forEach(change => {
if (change === 'full-update') {
// Full update logic
} else {
element[change.key] = change.value;
}
});
}
}
// Usage
const vdom = new VirtualDOM();
// Update only changed properties
vdom.updateElement('user-card', {
textContent: 'John Updated',
className: 'user active',
'data-id': 123
});
Event Delegation
javascript
// BAD: Event listener on each element
const buttons = document.querySelectorAll('.btn');
buttons.forEach(btn => {
btn.addEventListener('click', handleClick); // Multiple listeners
});
// GOOD: Single event listener on parent
document.getElementById('container').addEventListener('click', (event) => {
if (event.target.matches('.btn')) {
handleClick(event);
}
});
// BETTER: For dynamic content
class EventDelegator {
constructor() {
this.handlers = new Map();
}
addDelegatedListener(container, selector, eventType, handler) {
const key = `${eventType}-${selector}`;
if (!this.handlers.has(key)) {
const listener = (event) => {
if (event.target.matches(selector)) {
handler(event);
}
// Check parent elements too (for nested content)
let element = event.target;
while (element && element !== container) {
if (element.matches(selector)) {
handler.call(element, event);
break;
}
element = element.parentElement;
}
};
container.addEventListener(eventType, listener);
this.handlers.set(key, listener);
}
}
removeDelegatedListener(container, selector, eventType) {
const key = `${eventType}-${selector}`;
const listener = this.handlers.get(key);
if (listener) {
container.removeEventListener(eventType, listener);
this.handlers.delete(key);
}
}
}
// Usage
const delegator = new EventDelegator();
const container = document.getElementById('dynamic-content');
// Add delegated listener
delegator.addDelegatedListener(container, '.btn', 'click', function(event) {
console.log('Button clicked:', this.textContent);
});
2. Debouncing & Throttling
Debouncing Implementation
javascript
// Basic debounce
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func.apply(this, args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Advanced debounce with immediate option
function advancedDebounce(func, wait, immediate = false) {
let timeout;
let result;
return function(...args) {
const context = this;
const later = function() {
timeout = null;
if (!immediate) {
result = func.apply(context, args);
}
};
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) {
result = func.apply(context, args);
}
return result;
};
}
// Usage examples
// Search input
const searchInput = document.getElementById('search');
const performSearch = debounce(function(query) {
console.log('Searching for:', query);
// API call here
}, 300);
searchInput.addEventListener('input', (e) => {
performSearch(e.target.value);
});
// Window resize
const handleResize = debounce(function() {
console.log('Window resized to:', window.innerWidth, 'x', window.innerHeight);
// Update layout
}, 250);
window.addEventListener('resize', handleResize);
// Form validation on blur
const validateField = debounce(function(value) {
console.log('Validating:', value);
// Validation logic
}, 500);
document.getElementById('email').addEventListener('input', (e) => {
validateField(e.target.value);
});
Throttling Implementation
javascript
// Basic throttle
function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// Advanced throttle with trailing call
function advancedThrottle(func, limit, options = {}) {
let lastCall = 0;
let timeout;
let lastArgs;
let lastContext;
const { leading = true, trailing = true } = options;
return function(...args) {
const context = this;
const now = Date.now();
if (!lastCall && !leading) {
lastCall = now;
}
const remaining = limit - (now - lastCall);
if (remaining <= 0 || remaining > limit) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
lastCall = now;
func.apply(context, args);
} else if (!timeout && trailing) {
timeout = setTimeout(() => {
lastCall = leading ? Date.now() : 0;
timeout = null;
if (lastArgs) {
func.apply(lastContext, lastArgs);
lastArgs = lastContext = null;
}
}, remaining);
lastArgs = args;
lastContext = context;
}
};
}
// Usage examples
// Scroll events
const handleScroll = throttle(function() {
const scrollTop = window.pageYOffset;
console.log('Scroll position:', scrollTop);
// Update sticky header, lazy load, etc.
updateHeaderVisibility(scrollTop);
}, 100);
window.addEventListener('scroll', handleScroll);
// Mouse move tracking
const trackMouse = throttle(function(event) {
console.log('Mouse at:', event.clientX, event.clientY);
// Update cursor follower or tooltip
}, 50);
document.addEventListener('mousemove', trackMouse);
// Game loop or animation
const gameUpdate = throttle(function() {
// Update game state
updatePhysics();
renderFrame();
}, 16); // ~60 FPS
Debounce vs Throttle Visualizer
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Debounce vs Throttle</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
header {
text-align: center;
margin-bottom: 40px;
}
h1 {
font-size: 2.5rem;
margin-bottom: 10px;
}
.controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 40px;
}
.control-card {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 15px;
padding: 25px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
.control-card h3 {
font-size: 1.5rem;
margin-bottom: 15px;
color: #ffcc00;
}
.slider-container {
margin: 20px 0;
}
.slider-container label {
display: block;
margin-bottom: 8px;
font-weight: 600;
}
.slider-value {
float: right;
font-weight: bold;
}
input[type="range"] {
width: 100%;
height: 10px;
border-radius: 5px;
background: rgba(255, 255, 255, 0.2);
outline: none;
-webkit-appearance: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: #ffcc00;
cursor: pointer;
}
.visualizer {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 40px;
}
.chart-container {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 15px;
padding: 20px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
.chart {
height: 200px;
position: relative;
margin-top: 20px;
}
.event-bar {
position: absolute;
bottom: 0;
width: 4px;
background: rgba(255, 255, 255, 0.5);
}
.execution-bar {
position: absolute;
bottom: 30px;
width: 8px;
background: #ff4444;
border-radius: 4px;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-top: 30px;
}
.stat-card {
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
padding: 15px;
text-align: center;
}
.stat-value {
font-size: 2rem;
font-weight: bold;
color: #ffcc00;
}
.stat-label {
font-size: 0.9rem;
opacity: 0.8;
}
.legend {
display: flex;
gap: 20px;
justify-content: center;
margin-top: 20px;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
}
.legend-color {
width: 15px;
height: 15px;
border-radius: 3px;
}
.event-color {
background: rgba(255, 255, 255, 0.5);
}
.debounce-color {
background: #4CAF50;
}
.throttle-color {
background: #2196F3;
}
button {
background: #ffcc00;
color: #333;
border: none;
padding: 12px 24px;
border-radius: 25px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
width: 100%;
margin-top: 20px;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(255, 204, 0, 0.4);
}
.description {
margin-top: 30px;
line-height: 1.6;
background: rgba(255, 255, 255, 0.1);
padding: 20px;
border-radius: 10px;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>⚡ Debounce vs Throttle Visualizer</h1>
<p>See the difference between these two performance optimization techniques</p>
</header>
<div class="controls">
<div class="control-card">
<h3>🎮 Simulate Events</h3>
<p>Move mouse in this area to generate events:</p>
<div id="event-area" style="
height: 150px;
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
margin: 15px 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
cursor: crosshair;
">
Move Mouse Here
</div>
<button onclick="simulateRapidEvents()">Simulate Rapid Events</button>
<button onclick="resetVisualizer()" style="background: #ff4444; color: white;">Reset</button>
</div>
<div class="control-card">
<h3>⚙️ Configuration</h3>
<div class="slider-container">
<label>
<span>Debounce Delay</span>
<span class="slider-value" id="debounce-value">300ms</span>
</label>
<input type="range" id="debounce-slider" min="0" max="1000" value="300" step="10">
</div>
<div class="slider-container">
<label>
<span>Throttle Interval</span>
<span class="slider-value" id="throttle-value">100ms</span>
</label>
<input type="range" id="throttle-slider" min="0" max="1000" value="100" step="10">
</div>
<div class="slider-container">
<label>
<span>Event Rate (Simulation)</span>
<span class="slider-value" id="rate-value">20ms</span>
</label>
<input type="range" id="rate-slider" min="1" max="100" value="20" step="1">
</div>
</div>
</div>
<div class="visualizer">
<div class="chart-container">
<h3>📈 Raw Events</h3>
<div class="chart" id="raw-chart"></div>
<div class="stats">
<div class="stat-card">
<div class="stat-value" id="raw-count">0</div>
<div class="stat-label">Total Events</div>
</div>
<div class="stat-card">
<div class="stat-value" id="raw-rate">0</div>
<div class="stat-label">Events/Second</div>
</div>
</div>
</div>
<div class="chart-container">
<h3>🎯 Debounced Events</h3>
<div class="chart" id="debounce-chart"></div>
<div class="stats">
<div class="stat-card">
<div class="stat-value" id="debounce-count">0</div>
<div class="stat-label">Executions</div>
</div>
<div class="stat-card">
<div class="stat-value" id="debounce-savings">0%</div>
<div class="stat-label">Reduction</div>
</div>
</div>
</div>
<div class="chart-container">
<h3>⚡ Throttled Events</h3>
<div class="chart" id="throttle-chart"></div>
<div class="stats">
<div class="stat-card">
<div class="stat-value" id="throttle-count">0</div>
<div class="stat-label">Executions</div>
</div>
<div class="stat-card">
<div class="stat-value" id="throttle-savings">0%</div>
<div class="stat-label">Reduction</div>
</div>
</div>
</div>
</div>
<div class="legend">
<div class="legend-item">
<div class="legend-color event-color"></div>
<span>Event Triggered</span>
</div>
<div class="legend-item">
<div class="legend-color debounce-color"></div>
<span>Debounced Execution</span>
</div>
<div class="legend-item">
<div class="legend-color throttle-color"></div>
<span>Throttled Execution</span>
</div>
</div>
<div class="description">
<h3>📖 When to Use Each:</h3>
<p><strong>Debouncing</strong> groups multiple sequential calls into a single execution. Use for: search inputs, window resize, form validation.</p>
<p><strong>Throttling</strong> guarantees execution at regular intervals. Use for: scrolling, mouse movements, game loops, API rate limiting.</p>
</div>
</div>
<script>
class PerformanceVisualizer {
constructor() {
this.rawEvents = [];
this.debouncedExecutions = [];
this.throttledExecutions = [];
this.eventCount = 0;
this.debounceCount = 0;
this.throttleCount = 0;
this.startTime = Date.now();
// Get configuration elements
this.debounceSlider = document.getElementById('debounce-slider');
this.throttleSlider = document.getElementById('throttle-slider');
this.rateSlider = document.getElementById('rate-slider');
// Initialize charts
this.rawChart = document.getElementById('raw-chart');
this.debounceChart = document.getElementById('debounce-chart');
this.throttleChart = document.getElementById('throttle-chart');
// Setup event listeners
this.setupSliders();
this.setupEventArea();
this.setupDebounceThrottle();
// Start update loop
this.updateCharts();
this.updateStats();
}
setupSliders() {
// Update slider values display
this.debounceSlider.addEventListener('input', (e) => {
document.getElementById('debounce-value').textContent = e.target.value + 'ms';
this.setupDebounceThrottle();
});
this.throttleSlider.addEventListener('input', (e) => {
document.getElementById('throttle-value').textContent = e.target.value + 'ms';
this.setupDebounceThrottle();
});
this.rateSlider.addEventListener('input', (e) => {
document.getElementById('rate-value').textContent = e.target.value + 'ms';
});
}
setupEventArea() {
const eventArea = document.getElementById('event-area');
let mouseMoveTimeout;
eventArea.addEventListener('mousemove', (e) => {
// Record event
this.recordEvent('mouse');
// Debounced handler
this.debouncedHandler();
// Throttled handler
this.throttledHandler();
// Visual feedback
clearTimeout(mouseMoveTimeout);
eventArea.style.background = 'rgba(255, 204, 0, 0.2)';
mouseMoveTimeout = setTimeout(() => {
eventArea.style.background = 'rgba(255, 255, 255, 0.1)';
}, 100);
});
}
setupDebounceThrottle() {
const debounceDelay = parseInt(this.debounceSlider.value);
const throttleInterval = parseInt(this.throttleSlider.value);
// Create debounced function
this.debouncedHandler = this.debounce(() => {
this.recordDebouncedExecution();
}, debounceDelay);
// Create throttled function
this.throttledHandler = this.throttle(() => {
this.recordThrottledExecution();
}, throttleInterval);
}
// Debounce implementation
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func.apply(this, args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Throttle implementation
throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
recordEvent(source) {
this.eventCount++;
this.rawEvents.push({
time: Date.now(),
source
});
// Keep only recent events for visualization
const oneSecondAgo = Date.now() - 1000;
this.rawEvents = this.rawEvents.filter(event => event.time > oneSecondAgo);
}
recordDebouncedExecution() {
this.debounceCount++;
this.debouncedExecutions.push({
time: Date.now(),
type: 'debounce'
});
// Keep only recent executions
const fiveSecondsAgo = Date.now() - 5000;
this.debouncedExecutions = this.debouncedExecutions.filter(exec => exec.time > fiveSecondsAgo);
}
recordThrottledExecution() {
this.throttleCount++;
this.throttledExecutions.push({
time: Date.now(),
type: 'throttle'
});
// Keep only recent executions
const fiveSecondsAgo = Date.now() - 5000;
this.throttledExecutions = this.throttledExecutions.filter(exec => exec.time > fiveSecondsAgo);
}
simulateRapidEvents() {
const eventCount = 100;
const interval = parseInt(this.rateSlider.value);
for (let i = 0; i < eventCount; i++) {
setTimeout(() => {
this.recordEvent('simulation');
this.debouncedHandler();
this.throttledHandler();
}, i * interval);
}
}
updateCharts() {
// Clear charts
this.rawChart.innerHTML = '';
this.debounceChart.innerHTML = '';
this.throttleChart.innerHTML = '';
const now = Date.now();
const chartWidth = this.rawChart.clientWidth;
const fiveSeconds = 5000;
// Draw raw events
this.rawEvents.forEach(event => {
const position = ((event.time - (now - fiveSeconds)) / fiveSeconds) * chartWidth;
if (position >= 0 && position <= chartWidth) {
const bar = document.createElement('div');
bar.className = 'event-bar';
bar.style.left = position + 'px';
bar.style.height = '40px';
this.rawChart.appendChild(bar);
}
});
// Draw debounced executions
this.debouncedExecutions.forEach(exec => {
const position = ((exec.time - (now - fiveSeconds)) / fiveSeconds) * chartWidth;
if (position >= 0 && position <= chartWidth) {
const bar = document.createElement('div');
bar.className = 'execution-bar';
bar.style.left = position + 'px';
bar.style.height = '60px';
bar.style.background = '#4CAF50';
this.debounceChart.appendChild(bar);
}
});
// Draw throttled executions
this.throttledExecutions.forEach(exec => {
const position = ((exec.time - (now - fiveSeconds)) / fiveSeconds) * chartWidth;
if (position >= 0 && position <= chartWidth) {
const bar = document.createElement('div');
bar.className = 'execution-bar';
bar.style.left = position + 'px';
bar.style.height = '60px';
bar.style.background = '#2196F3';
this.throttleChart.appendChild(bar);
}
});
// Schedule next update
requestAnimationFrame(() => this.updateCharts());
}
updateStats() {
const now = Date.now();
const elapsedSeconds = (now - this.startTime) / 1000;
// Calculate events per second (last 1 second)
const oneSecondAgo = now - 1000;
const recentEvents = this.rawEvents.filter(event => event.time > oneSecondAgo);
const eventsPerSecond = recentEvents.length;
// Update displays
document.getElementById('raw-count').textContent = this.eventCount;
document.getElementById('raw-rate').textContent = eventsPerSecond;
document.getElementById('debounce-count').textContent = this.debounceCount;
document.getElementById('throttle-count').textContent = this.throttleCount;
// Calculate savings
if (this.eventCount > 0) {
const debounceSavings = Math.round((1 - this.debounceCount / this.eventCount) * 100);
const throttleSavings = Math.round((1 - this.throttleCount / this.eventCount) * 100);
document.getElementById('debounce-savings').textContent = debounceSavings + '%';
document.getElementById('throttle-savings').textContent = throttleSavings + '%';
}
// Schedule next update
setTimeout(() => this.updateStats(), 1000);
}
resetVisualizer() {
this.rawEvents = [];
this.debouncedExecutions = [];
this.throttledExecutions = [];
this.eventCount = 0;
this.debounceCount = 0;
this.throttleCount = 0;
this.startTime = Date.now();
}
}
// Initialize visualizer
const visualizer = new PerformanceVisualizer();
// Expose simulation function globally
window.simulateRapidEvents = () => visualizer.simulateRapidEvents();
window.resetVisualizer = () => visualizer.resetVisualizer();
</script>
</body>
</html>
3. Lazy Loading
Image Lazy Loading
javascript
// Intersection Observer for lazy loading
class LazyLoader {
constructor(options = {}) {
this.options = {
root: null,
rootMargin: '50px',
threshold: 0.01,
...options
};
this.observer = new IntersectionObserver(
this.handleIntersection.bind(this),
this.options
);
this.observedElements = new Map();
}
observe(element, callback) {
if (element.dataset.lazyObserved) {
return; // Already observing
}
element.dataset.lazyObserved = 'true';
this.observedElements.set(element, callback);
this.observer.observe(element);
}
unobserve(element) {
if (element.dataset.lazyObserved) {
this.observer.unobserve(element);
this.observedElements.delete(element);
delete element.dataset.lazyObserved;
}
}
handleIntersection(entries) {
entries.forEach(entry => {
if (entry.isIntersecting) {
const element = entry.target;
const callback = this.observedElements.get(element);
if (callback) {
callback(element);
this.unobserve(element);
}
}
});
}
disconnect() {
this.observer.disconnect();
this.observedElements.clear();
}
}
// Image lazy loading implementation
class LazyImageLoader extends LazyLoader {
constructor(options = {}) {
super(options);
}
addImage(imgElement) {
if (!imgElement.dataset.src) {
console.warn('Image element missing data-src attribute');
return;
}
// Set placeholder or low-quality image
if (imgElement.dataset.lowSrc) {
imgElement.src = imgElement.dataset.lowSrc;
} else {
imgElement.src = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="%WIDTH%" height="%HEIGHT%" viewBox="0 0 %WIDTH% %HEIGHT%"%3E%3Crect width="100%" height="100%" fill="%23f0f0f0"/%3E%3C/svg%3E'
.replace(/%WIDTH%/g, imgElement.width || 300)
.replace(/%HEIGHT%/g, imgElement.height || 200);
}
// Add loading class for styling
imgElement.classList.add('lazy-loading');
// Observe element
this.observe(imgElement, (element) => {
this.loadImage(element);
});
}
loadImage(imgElement) {
const src = imgElement.dataset.src;
const srcset = imgElement.dataset.srcset;
// Create new image to preload
const tempImage = new Image();
tempImage.onload = () => {
// Apply the real image
imgElement.src = src;
if (srcset) {
imgElement.srcset = srcset;
}
// Remove loading class, add loaded class
imgElement.classList.remove('lazy-loading');
imgElement.classList.add('lazy-loaded');
// Dispatch custom event
imgElement.dispatchEvent(new CustomEvent('lazyloaded', {
detail: { element: imgElement }
}));
};
tempImage.onerror = () => {
console.error('Failed to load image:', src);
imgElement.classList.remove('lazy-loading');
imgElement.classList.add('lazy-error');
// Fallback to error image if specified
if (imgElement.dataset.errorSrc) {
imgElement.src = imgElement.dataset.errorSrc;
}
};
// Start loading
tempImage.src = src;
if (srcset) {
tempImage.srcset = srcset;
}
}
// Initialize all images on page
initAllImages() {
document.querySelectorAll('img[data-src]').forEach(img => {
this.addImage(img);
});
}
}
// Usage
const imageLoader = new LazyImageLoader();
document.addEventListener('DOMContentLoaded', () => {
imageLoader.initAllImages();
});
// For dynamically added images
function addNewImage(src, container) {
const img = document.createElement('img');
img.dataset.src = src;
img.width = 300;
img.height = 200;
img.alt = 'Lazy loaded image';
container.appendChild(img);
imageLoader.addImage(img);
}
Component Lazy Loading
javascript
// Lazy load JavaScript components
class ComponentLoader {
constructor() {
this.components = new Map();
this.loadedComponents = new Set();
this.observer = new IntersectionObserver(
this.handleComponentIntersection.bind(this),
{ rootMargin: '100px', threshold: 0.1 }
);
}
registerComponent(name, loader) {
this.components.set(name, loader);
}
observeComponent(element, componentName) {
if (!this.components.has(componentName)) {
console.warn(`Component "${componentName}" not registered`);
return;
}
element.dataset.component = componentName;
this.observer.observe(element);
}
async handleComponentIntersection(entries) {
for (const entry of entries) {
if (entry.isIntersecting) {
const element = entry.target;
const componentName = element.dataset.component;
if (componentName && !this.loadedComponents.has(componentName)) {
await this.loadComponent(componentName, element);
this.observer.unobserve(element);
}
}
}
}
async loadComponent(name, container) {
console.log(`Loading component: ${name}`);
const loader = this.components.get(name);
if (!loader) {
throw new Error(`Loader not found for component: ${name}`);
}
try {
// Show loading state
container.classList.add('component-loading');
// Load component
const component = await loader();
// Initialize component in container
if (typeof component === 'function') {
component(container);
} else if (component.default && typeof component.default === 'function') {
component.default(container);
} else {
container.innerHTML = component;
}
// Update state
container.classList.remove('component-loading');
container.classList.add('component-loaded');
this.loadedComponents.add(name);
console.log(`Component "${name}" loaded successfully`);
} catch (error) {
console.error(`Failed to load component "${name}":`, error);
container.classList.add('component-error');
container.innerHTML = `
<div class="error-message">
Failed to load component: ${name}
</div>
`;
}
}
// Initialize all components on page
initComponents() {
document.querySelectorAll('[data-component]').forEach(element => {
const componentName = element.dataset.component;
this.observeComponent(element, componentName);
});
}
}
// Usage example
const componentLoader = new ComponentLoader();
// Register components
componentLoader.registerComponent('chart', async () => {
// Dynamically import chart library
const { Chart } = await import('https://cdn.jsdelivr.net/npm/chart.js');
return (container) => {
const ctx = container.querySelector('canvas').getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
labels: ['A', 'B', 'C', 'D'],
datasets: [{
label: 'Data',
data: [12, 19, 3, 5],
backgroundColor: 'rgba(75, 192, 192, 0.2)',
borderColor: 'rgba(75, 192, 192, 1)',
borderWidth: 1
}]
}
});
};
});
componentLoader.registerComponent('map', async () => {
// Load map component
const mapModule = await import('./components/map.js');
return mapModule.default;
});
componentLoader.registerComponent('comments', async () => {
// Load comments from API
const response = await fetch('/api/comments');
const comments = await response.json();
return (container) => {
container.innerHTML = comments.map(comment => `
<div class="comment">
<h4>${comment.author}</h4>
<p>${comment.text}</p>
</div>
`).join('');
};
});
// Initialize on page load
document.addEventListener('DOMContentLoaded', () => {
componentLoader.initComponents();
});
4. Code Splitting
Dynamic Imports (ES Modules)
javascript
// Basic dynamic import
button.addEventListener('click', async () => {
try {
// Load module on demand
const module = await import('./heavy-module.js');
module.doSomething();
} catch (error) {
console.error('Failed to load module:', error);
}
});
// Route-based code splitting (for SPAs)
class RouteManager {
constructor() {
this.routes = new Map();
this.loadedRoutes = new Set();
}
addRoute(path, modulePath) {
this.routes.set(path, modulePath);
}
async navigate(path) {
if (this.loadedRoutes.has(path)) {
// Route already loaded
return;
}
const modulePath = this.routes.get(path);
if (!modulePath) {
throw new Error(`Route not found: ${path}`);
}
try {
// Show loading indicator
this.showLoading();
// Dynamically import route module
const module = await import(modulePath);
// Initialize route
if (module.default && typeof module.default === 'function') {
await module.default();
}
// Update state
this.loadedRoutes.add(path);
this.hideLoading();
} catch (error) {
console.error(`Failed to load route ${path}:`, error);
this.showError(`Failed to load page`);
}
}
showLoading() {
// Show loading spinner
const loader = document.createElement('div');
loader.id = 'route-loader';
loader.innerHTML = `
<div class="spinner"></div>
<p>Loading...</p>
`;
document.body.appendChild(loader);
}
hideLoading() {
const loader = document.getElementById('route-loader');
if (loader) {
loader.remove();
}
}
showError(message) {
// Show error message
const errorDiv = document.createElement('div');
errorDiv.className = 'route-error';
errorDiv.textContent = message;
document.body.appendChild(errorDiv);
}
}
// Usage
const routeManager = new RouteManager();
routeManager.addRoute('/dashboard', './routes/dashboard.js');
routeManager.addRoute('/analytics', './routes/analytics.js');
routeManager.addRoute('/settings', './routes/settings.js');
// Navigation
document.querySelectorAll('.nav-link').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const path = link.getAttribute('href');
routeManager.navigate(path);
});
});
Webpack-Style Code Splitting
javascript
// With Webpack magic comments (for bundlers)
const loadModule = (moduleName) => {
return import(
/* webpackChunkName: "[request]" */
/* webpackPrefetch: true */
/* webpackPreload: true */
`./modules/${moduleName}`
);
};
// Component-based splitting
const ComponentLoader = {
cache: new Map(),
async load(componentName) {
// Check cache first
if (this.cache.has(componentName)) {
return this.cache.get(componentName);
}
// Dynamic import with error handling
try {
const module = await import(
/* webpackChunkName: "component-[request]" */
`./components/${componentName}.js`
);
// Cache the module
this.cache.set(componentName, module);
return module;
} catch (error) {
console.error(`Failed to load component ${componentName}:`, error);
// Return fallback component
return {
default: (container) => {
container.innerHTML = `
<div class="component-error">
Failed to load component: ${componentName}
</div>
`;
}
};
}
}
};
// Lazy load based on user interaction
class PredictiveLoader {
constructor() {
this.observers = new Map();
this.predictedModules = new Set();
this.preloadedModules = new Set();
}
// Observe user behavior to predict what to load
observeUserBehavior(element, moduleName) {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.preloadModule(moduleName);
}
});
},
{ rootMargin: '200px', threshold: 0.1 }
);
observer.observe(element);
this.observers.set(moduleName, observer);
}
// Preload module without executing
async preloadModule(moduleName) {
if (this.preloadedModules.has(moduleName)) {
return;
}
console.log(`Preloading module: ${moduleName}`);
try {
// Use link rel="preload" for CSS/JS
if (moduleName.endsWith('.css')) {
this.preloadCSS(moduleName);
} else if (moduleName.endsWith('.js')) {
await this.preloadJS(moduleName);
}
this.preloadedModules.add(moduleName);
} catch (error) {
console.error(`Failed to preload ${moduleName}:`, error);
}
}
preloadCSS(href) {
const link = document.createElement('link');
link.rel = 'preload';
link.as = 'style';
link.href = href;
link.onload = () => {
// Convert preload to stylesheet
link.rel = 'stylesheet';
};
document.head.appendChild(link);
}
async preloadJS(src) {
// Use preload for JavaScript
const link = document.createElement('link');
link.rel = 'preload';
link.as = 'script';
link.href = src;
document.head.appendChild(link);
// Or use prefetch for lower priority
// link.rel = 'prefetch';
}
// Clean up
disconnect() {
this.observers.forEach(observer => observer.disconnect());
this.observers.clear();
}
}
Performance Monitoring
javascript
// Performance monitoring utilities
class PerformanceMonitor {
constructor() {
this.metrics = new Map();
this.resourceTimings = [];
this.longTasks = [];
this.setupObservers();
}
setupObservers() {
// Observe Performance Timeline
if ('PerformanceObserver' in window) {
// Long tasks observer
const longTaskObserver = new PerformanceObserver((list) => {
list.getEntries().forEach(entry => {
if (entry.duration > 50) { // Tasks longer than 50ms
this.longTasks.push(entry);
this.reportLongTask(entry);
}
});
});
longTaskObserver.observe({ entryTypes: ['longtask'] });
// Layout shift observer
const layoutShiftObserver = new PerformanceObserver((list) => {
list.getEntries().forEach(entry => {
if (!entry.hadRecentInput) {
this.metrics.set('CLS',
(this.metrics.get('CLS') || 0) + entry.value
);
}
});
});
layoutShiftObserver.observe({ entryTypes: ['layout-shift'] });
// Largest Contentful Paint observer
const lcpObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
this.metrics.set('LCP', lastEntry.startTime);
});
lcpObserver.observe({ entryTypes: ['largest-contentful-paint'] });
}
// Monitor resource loading
this.monitorResourceTiming();
}
monitorResourceTiming() {
// Get all resource timing entries
const resources = performance.getEntriesByType('resource');
this.resourceTimings = resources.map(resource => ({
name: resource.name,
duration: resource.duration,
transferSize: resource.transferSize,
initiatorType: resource.initiatorType
}));
// Log slow resources
const slowResources = resources.filter(r => r.duration > 1000);
if (slowResources.length > 0) {
console.warn('Slow resources detected:', slowResources);
}
}
reportLongTask(task) {
console.warn(`Long task detected: ${task.duration.toFixed(2)}ms`, task);
// Send to analytics
this.sendToAnalytics('long_task', {
duration: task.duration,
startTime: task.startTime,
attribution: task.attribution
});
}
measureFunction(func, name) {
const start = performance.now();
const result = func();
const duration = performance.now() - start;
this.metrics.set(name, duration);
if (duration > 100) {
console.warn(`Slow function "${name}": ${duration.toFixed(2)}ms`);
}
return result;
}
async measureAsyncFunction(func, name) {
const start = performance.now();
const result = await func();
const duration = performance.now() - start;
this.metrics.set(name, duration);
if (duration > 500) {
console.warn(`Slow async function "${name}": ${duration.toFixed(2)}ms`);
}
return result;
}
getMetrics() {
// Get Core Web Vitals
const fcp = performance.getEntriesByName('first-contentful-paint')[0];
const lcp = this.metrics.get('LCP');
const fid = performance.getEntriesByName('first-input')[0];
const cls = this.metrics.get('CLS');
return {
fcp: fcp?.startTime,
lcp,
fid: fid?.duration,
cls,
longTasks: this.longTasks.length,
resources: this.resourceTimings.length,
// Custom metrics
customMetrics: Object.fromEntries(this.metrics)
};
}
sendToAnalytics(event, data) {
// Send to your analytics service
if (typeof gtag === 'function') {
gtag('event', event, data);
}
// Or use navigator.sendBeacon for better performance
if (navigator.sendBeacon) {
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
navigator.sendBeacon('/analytics', blob);
}
}
// Memory monitoring
monitorMemory() {
if ('memory' in performance) {
setInterval(() => {
const memory = performance.memory;
console.log('Memory usage:', {
used: Math.round(memory.usedJSHeapSize / 1024 / 1024) + 'MB',
total: Math.round(memory.totalJSHeapSize / 1024 / 1024) + 'MB',
limit: Math.round(memory.jsHeapSizeLimit / 1024 / 1024) + 'MB'
});
// Warn if memory usage is high
if (memory.usedJSHeapSize / memory.totalJSHeapSize > 0.8) {
console.warn('High memory usage detected!');
}
}, 10000); // Check every 10 seconds
}
}
}
// Usage
const perfMonitor = new PerformanceMonitor();
// Measure critical functions
const expensiveCalculation = perfMonitor.measureFunction(() => {
// Your expensive code here
}, 'expensiveCalculation');
// Get metrics and report
window.addEventListener('load', () => {
setTimeout(() => {
const metrics = perfMonitor.getMetrics();
console.log('Performance metrics:', metrics);
// Send to your analytics
perfMonitor.sendToAnalytics('performance_metrics', metrics);
}, 5000); // Wait a bit after load
});
Key Performance Optimization Tips
-
Minimize DOM Manipulation: Batch changes, use DocumentFragments
-
Debounce Frequent Events: Input, resize, scroll events
-
Throttle Animation Frames: Mouse move, scroll handlers
-
Lazy Load Images: Use Intersection Observer
-
Split Code: Dynamic imports for non-critical features
-
Cache Results: Memoize expensive calculations
-
Use Web Workers: Offload heavy computations
-
Optimize Loops: Reduce complexity, break early
-
Avoid Forced Synchronous Layout: Batch read/write operations
-
Monitor Performance: Use Performance API to identify bottlenecks
Note: Measure before you optimize. Use Chrome DevTools Performance tab to identify actual bottlenecks rather than guessing what might be slow.
Comments (0)
No comments yet. Be the first to comment!