Web Performance Optimization Guide
Master web performance optimization with Core Web Vitals, loading strategies, and proven techniques. Learn to measure, monitor, and improve your site's speed for better user experience and SEO.
Core Web Vitals
Google's essential metrics for measuring user experience quality.
Largest Contentful Paint (LCP)
Loading performance - How fast the main content loads
// Measure LCP with PerformanceObserver
new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
console.log('LCP:', lastEntry.startTime);
}).observe({ entryTypes: ['largest-contentful-paint'] });
// Common LCP elements:
// - Images (img, svg)
// - Video poster images
// - Background images (via url())
// - Block-level text elements
Cumulative Layout Shift (CLS)
Visual stability - How much the page shifts during loading
/* Prevent layout shifts */
img, video {
/* Always include dimensions */
width: 100%;
height: auto;
aspect-ratio: 16 / 9;
}
/* Reserve space for dynamic content */
.ad-container {
min-height: 250px;
}
/* Use CSS containment */
.dynamic-content {
contain: layout style paint;
}
/* Avoid inserting content above existing content */
/* Use transform instead of top/left for animations */
Interaction to Next Paint (INP)
Responsiveness - How quickly the page responds to interactions
Replacing FID// Optimize JavaScript execution
// Break up long tasks
function processLargeArray(array) {
const chunkSize = 100;
let index = 0;
function processChunk() {
const chunk = array.slice(index, index + chunkSize);
// Process chunk
chunk.forEach(item => processItem(item));
index += chunkSize;
if (index < array.length) {
// Yield to main thread
requestIdleCallback(processChunk);
}
}
processChunk();
}
// Use Web Workers for heavy computation
const worker = new Worker('heavy-computation.js');
worker.postMessage({ cmd: 'process', data: largeDataSet });
Why Core Web Vitals Matter: They're ranking factors for Google Search and directly impact user experience. Poor scores lead to higher bounce rates and lower conversions.
Loading Performance
Optimize how and when resources are loaded to improve perceived performance.
Resource Hints
<!-- DNS Prefetch - Resolve DNS early -->
<link rel="dns-prefetch" href="//api.example.com">
<!-- Preconnect - DNS + TCP + TLS -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- Prefetch - Download for future navigation -->
<link rel="prefetch" href="/next-page.html">
<!-- Preload - High priority resource for current page -->
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/css/critical.css" as="style">
<link rel="preload" href="/js/app.js" as="script">
<!-- Modulepreload - Preload ES modules -->
<link rel="modulepreload" href="/js/module.js">
Script Loading Strategies
<script src="app.js"></script>
Blocks HTML parsing and rendering
<script async src="app.js"></script>
Downloads in parallel, executes immediately
<script defer src="app.js"></script>
Downloads in parallel, executes after DOM
Resource Loading Waterfall
Lazy Loading
<!-- Native lazy loading for images -->
<img src="hero.jpg" alt="Hero image" loading="eager"> <!-- Above the fold -->
<img src="product.jpg" alt="Product" loading="lazy"> <!-- Below the fold -->
<!-- Native lazy loading for iframes -->
<iframe src="video.html" loading="lazy"></iframe>
<!-- Intersection Observer for custom lazy loading -->
<script>
const lazyImages = document.querySelectorAll('img[data-src]');
const imageObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.add('loaded');
imageObserver.unobserve(img);
}
});
}, {
rootMargin: '50px 0px' // Start loading 50px before visible
});
lazyImages.forEach(img => imageObserver.observe(img));
</script>
Lazy Loading Visualization
Priority Hints
<!-- Fetch Priority API (New) -->
<img src="hero.jpg" fetchpriority="high" alt="Hero">
<img src="footer-logo.jpg" fetchpriority="low" alt="Logo">
<!-- Adjust script priority -->
<script src="critical.js" fetchpriority="high"></script>
<script src="analytics.js" fetchpriority="low" async></script>
<!-- Link priority -->
<link rel="stylesheet" href="critical.css" fetchpriority="high">
Runtime Performance
Optimize JavaScript execution and rendering for smooth interactions.
JavaScript Optimization
// Debounce expensive operations
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Usage
const expensiveOperation = debounce(() => {
// Heavy computation
}, 300);
// Throttle continuous events
function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// Use requestAnimationFrame for visual updates
function smoothScroll(element, target, duration) {
const start = element.scrollTop;
const change = target - start;
const startTime = performance.now();
function animateScroll(currentTime) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
element.scrollTop = start + change * easeInOutQuad(progress);
if (progress < 1) {
requestAnimationFrame(animateScroll);
}
}
requestAnimationFrame(animateScroll);
}
Virtual Scrolling
// Simple virtual scrolling implementation
class VirtualScroller {
constructor(container, items, itemHeight) {
this.container = container;
this.items = items;
this.itemHeight = itemHeight;
this.visibleItems = Math.ceil(container.clientHeight / itemHeight);
this.totalHeight = items.length * itemHeight;
this.init();
}
init() {
// Create viewport
this.viewport = document.createElement('div');
this.viewport.style.height = `${this.totalHeight}px`;
// Create content container
this.content = document.createElement('div');
this.content.style.transform = 'translateY(0)';
this.container.appendChild(this.viewport);
this.container.appendChild(this.content);
this.container.addEventListener('scroll', () => this.render());
this.render();
}
render() {
const scrollTop = this.container.scrollTop;
const startIndex = Math.floor(scrollTop / this.itemHeight);
const endIndex = startIndex + this.visibleItems + 1;
// Clear content
this.content.innerHTML = '';
// Render visible items
for (let i = startIndex; i < endIndex && i < this.items.length; i++) {
const item = this.createItem(this.items[i], i);
this.content.appendChild(item);
}
// Position content
this.content.style.transform = `translateY(${startIndex * this.itemHeight}px)`;
}
createItem(data, index) {
const div = document.createElement('div');
div.style.height = `${this.itemHeight}px`;
div.textContent = data;
return div;
}
}
Memory Management
// Avoid memory leaks
class ComponentWithCleanup {
constructor() {
this.handleClick = this.handleClick.bind(this);
this.handleScroll = this.handleScroll.bind(this);
this.observers = new Set();
}
mount() {
// Add event listeners
document.addEventListener('click', this.handleClick);
window.addEventListener('scroll', this.handleScroll);
// Create observers
this.resizeObserver = new ResizeObserver(entries => {
// Handle resize
});
this.resizeObserver.observe(this.element);
}
unmount() {
// Remove event listeners
document.removeEventListener('click', this.handleClick);
window.removeEventListener('scroll', this.handleScroll);
// Disconnect observers
this.resizeObserver.disconnect();
// Clear references
this.observers.clear();
this.data = null;
}
}
// Use WeakMap for metadata
const metadata = new WeakMap();
function setMetadata(obj, data) {
metadata.set(obj, data);
}
// Object pooling for frequent allocations
class ObjectPool {
constructor(createFn, resetFn, maxSize = 100) {
this.createFn = createFn;
this.resetFn = resetFn;
this.pool = [];
this.maxSize = maxSize;
}
acquire() {
if (this.pool.length > 0) {
return this.pool.pop();
}
return this.createFn();
}
release(obj) {
if (this.pool.length < this.maxSize) {
this.resetFn(obj);
this.pool.push(obj);
}
}
}
Asset Optimization
Reduce file sizes and optimize delivery of images, fonts, and other assets.
Image Optimization
<!-- Modern image formats with fallbacks -->
<picture>
<source srcset="image.avif" type="image/avif">
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="Description"
loading="lazy"
decoding="async"
width="800"
height="600">
</picture>
<!-- Responsive images -->
<img srcset="small.jpg 480w,
medium.jpg 800w,
large.jpg 1200w"
sizes="(max-width: 600px) 100vw,
(max-width: 1200px) 50vw,
800px"
src="medium.jpg"
alt="Responsive image">
<!-- Art direction with picture -->
<picture>
<source media="(min-width: 1200px)" srcset="desktop.jpg">
<source media="(min-width: 768px)" srcset="tablet.jpg">
<img src="mobile.jpg" alt="Art directed image">
</picture>
| Format | Best For | Compression | Browser Support |
|---|---|---|---|
AVIF |
Photos, complex images | Best | Chrome, Firefox |
WebP |
Photos, graphics | Excellent | All modern browsers |
JPEG |
Photos | Good | Universal |
PNG |
Graphics, transparency | Fair | Universal |
SVG |
Icons, logos, simple graphics | Excellent | Universal |
Font Optimization
/* Preload critical fonts */
/* In HTML: */
/* Font display strategies */
@font-face {
font-family: 'CustomFont';
src: url('font.woff2') format('woff2'),
url('font.woff') format('woff');
font-display: swap; /* Show fallback immediately */
/* Other options:
- block: Hide text up to 3s (not recommended)
- fallback: Hide briefly, then fallback
- optional: Use if available immediately
*/
}
/* Variable fonts for multiple weights */
@font-face {
font-family: 'VariableFont';
src: url('variable.woff2') format('woff2-variations');
font-weight: 100 900; /* Full weight range */
}
/* Subset fonts for specific characters */
@font-face {
font-family: 'CustomFont';
src: url('font-subset.woff2') format('woff2');
unicode-range: U+0020-007E; /* Basic Latin */
}
- Use
woff2format (30% smaller than woff) - Subset fonts to needed characters
- Preload critical fonts
- Use
font-display: swapfor body text - Consider system font stack for performance
CSS Optimization
<!-- Critical CSS inline -->
<style>
/* Above-the-fold styles */
body { margin: 0; font-family: system-ui; }
.hero { height: 100vh; background: #000; }
</style>
<!-- Load non-critical CSS asynchronously -->
<link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="styles.css"></noscript>
<!-- Or use media attribute trick -->
<link rel="stylesheet" href="print.css" media="print" onload="this.media='all'">
JavaScript Bundle Optimization
// Code splitting with dynamic imports
// Instead of importing everything
import { heavyLibrary } from './heavy-library';
// Load on demand
button.addEventListener('click', async () => {
const { heavyLibrary } = await import('./heavy-library');
heavyLibrary.doSomething();
});
// Route-based code splitting (React example)
const HomePage = lazy(() => import('./pages/Home'));
const AboutPage = lazy(() => import('./pages/About'));
// Tree shaking - use specific imports
// Bad
import _ from 'lodash';
_.debounce();
// Good
import debounce from 'lodash/debounce';
debounce();
Critical Rendering Path
Optimize the sequence of steps browsers take to render content.
Optimizing the Critical Path
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- 1. Preconnect to required origins -->
<link rel="preconnect" href="https://fonts.gstatic.com">
<!-- 2. Inline critical CSS -->
<style>
/* Critical above-the-fold styles */
body { margin: 0; font-family: -apple-system, system-ui, sans-serif; }
.hero { min-height: 100vh; display: flex; align-items: center; }
</style>
<!-- 3. Preload critical resources -->
<link rel="preload" href="/fonts/main.woff2" as="font" crossorigin>
<link rel="preload" href="/js/app.js" as="script">
<!-- 4. Async load non-critical CSS -->
<link rel="preload" href="/css/main.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
</head>
<body>
<!-- 5. Inline critical content -->
<div class="hero">
<h1>Fast Loading Page</h1>
</div>
<!-- 6. Defer non-critical scripts -->
<script defer src="/js/app.js"></script>
<script defer src="/js/analytics.js"></script>
</body>
</html>
Render Blocking Resources
<link rel="stylesheet" href="styles.css">
<script src="app.js"></script>
Blocks first paint
<style>/* Critical CSS */</style>
<script defer src="app.js"></script>
Allows early paint
Caching Strategies
Implement effective caching to reduce server requests and improve load times.
HTTP Cache Headers
# Immutable assets (hashed filenames)
Cache-Control: public, max-age=31536000, immutable
# Static assets that might change
Cache-Control: public, max-age=86400, stale-while-revalidate=604800
# HTML documents
Cache-Control: no-cache, must-revalidate
# API responses
Cache-Control: private, max-age=300
# No caching
Cache-Control: no-store
Service Worker Caching
// sw.js - Service Worker with caching strategies
const CACHE_NAME = 'app-v1';
const urlsToCache = [
'/',
'/styles/main.css',
'/scripts/app.js',
'/offline.html'
];
// Install - cache assets
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(urlsToCache))
);
});
// Fetch strategies
self.addEventListener('fetch', event => {
const { request } = event;
const url = new URL(request.url);
// Network First (API calls)
if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirst(request));
}
// Cache First (static assets)
else if (request.destination === 'image' ||
request.destination === 'script' ||
request.destination === 'style') {
event.respondWith(cacheFirst(request));
}
// Stale While Revalidate (HTML)
else {
event.respondWith(staleWhileRevalidate(request));
}
});
// Cache First Strategy
async function cacheFirst(request) {
const cache = await caches.open(CACHE_NAME);
const cached = await cache.match(request);
if (cached) {
return cached;
}
const response = await fetch(request);
cache.put(request, response.clone());
return response;
}
// Network First Strategy
async function networkFirst(request) {
try {
const response = await fetch(request);
const cache = await caches.open(CACHE_NAME);
cache.put(request, response.clone());
return response;
} catch (error) {
const cached = await caches.match(request);
return cached || new Response('Offline', { status: 503 });
}
}
// Stale While Revalidate
async function staleWhileRevalidate(request) {
const cache = await caches.open(CACHE_NAME);
const cachedResponse = await cache.match(request);
const fetchPromise = fetch(request).then(response => {
cache.put(request, response.clone());
return response;
});
return cachedResponse || fetchPromise;
}
Browser Storage
// localStorage for small data (sync, 5-10MB limit)
localStorage.setItem('user-preferences', JSON.stringify(prefs));
// sessionStorage for temporary data
sessionStorage.setItem('form-data', JSON.stringify(formData));
// IndexedDB for large data (async, GB+ limit)
const dbPromise = indexedDB.open('AppDB', 1);
dbPromise.onsuccess = event => {
const db = event.target.result;
const transaction = db.transaction(['data'], 'readwrite');
const store = transaction.objectStore('data');
store.put({ id: 1, data: largeDataObject });
};
// Cache API for request/response pairs
caches.open('api-cache').then(cache => {
cache.put(request, response);
});
Performance Tools
Tools and APIs for measuring and monitoring performance.
Performance API
// Navigation Timing
const navTiming = performance.getEntriesByType('navigation')[0];
console.log('DOM Content Loaded:', navTiming.domContentLoadedEventEnd - navTiming.domContentLoadedEventStart);
console.log('Page Load Time:', navTiming.loadEventEnd - navTiming.fetchStart);
// Resource Timing
const resources = performance.getEntriesByType('resource');
resources.forEach(resource => {
console.log(`${resource.name}: ${resource.duration}ms`);
});
// User Timing API
performance.mark('myFunction-start');
myExpensiveFunction();
performance.mark('myFunction-end');
performance.measure('myFunction', 'myFunction-start', 'myFunction-end');
const measure = performance.getEntriesByName('myFunction')[0];
console.log(`myFunction took ${measure.duration}ms`);
// Performance Observer
const observer = new PerformanceObserver(list => {
list.getEntries().forEach(entry => {
console.log(`${entry.name}: ${entry.startTime}`);
});
});
observer.observe({
entryTypes: ['largest-contentful-paint', 'first-input', 'layout-shift']
});
Real User Monitoring (RUM)
// Collect Web Vitals
import { getCLS, getFID, getLCP, getTTFB, getINP } from 'web-vitals';
function sendToAnalytics(metric) {
// Send to your analytics endpoint
const body = JSON.stringify({
name: metric.name,
value: metric.value,
rating: metric.rating,
delta: metric.delta,
id: metric.id,
url: window.location.href,
timestamp: Date.now()
});
// Use sendBeacon for reliability
navigator.sendBeacon('/analytics', body);
}
getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getLCP(sendToAnalytics);
getTTFB(sendToAnalytics);
getINP(sendToAnalytics);
Chrome DevTools Performance
- Network Tab: Check waterfall, size, and timing
- Performance Tab: Record and analyze runtime performance
- Coverage Tab: Find unused CSS and JavaScript
- Lighthouse: Automated performance audits
- Rendering Tab: Paint flashing, layer borders, FPS meter
Performance Budget
// performance-budget.json
{
"timings": {
"firstContentfulPaint": 2000,
"largestContentfulPaint": 2500,
"timeToInteractive": 3500,
"totalBlockingTime": 300
},
"sizes": {
"bundle": {
"javascript": 300000,
"css": 60000,
"images": 500000,
"fonts": 100000,
"total": 1000000
}
},
"requests": {
"total": 50,
"thirdParty": 10
}
}
Best Practices
Essential performance optimization practices for modern web development.
Quick Wins Checklist
✅ Enable Compression
Use Gzip or Brotli compression
Content-Encoding: br
✅ Optimize Images
Use modern formats and proper sizing
<img loading="lazy" decoding="async">
✅ Minify Resources
Remove whitespace and comments
terser, cssnano, htmlmin
✅ Enable HTTP/2
Multiplexing and server push
Link: </style.css>; rel=preload
✅ Use CDN
Serve assets from edge locations
https://cdn.example.com/
✅ Remove Unused Code
Tree shake and purge CSS
PurgeCSS, Webpack tree shaking
Mobile Performance
// Adaptive loading based on connection
if ('connection' in navigator) {
const connection = navigator.connection;
// Check effective connection type
if (connection.effectiveType === '4g') {
// Load high quality images
loadHighResImages();
} else if (connection.effectiveType === '3g') {
// Load standard quality
loadStandardImages();
} else {
// Load low quality or placeholders
loadLowQualityImages();
}
// Monitor connection changes
connection.addEventListener('change', updateLoadingStrategy);
}
// Reduce JavaScript for low-end devices
if (navigator.hardwareConcurrency <= 4 ||
navigator.deviceMemory <= 4) {
// Load lighter experience
import('./lite-version.js');
} else {
// Load full experience
import('./full-version.js');
}
Third-Party Script Management
<!-- Load third-party scripts efficiently -->
<!-- 1. Use async for independent scripts -->
<script async src="https://www.google-analytics.com/analytics.js"></script>
<!-- 2. Lazy load non-critical third-party content -->
<script>
// Load after user interaction
let hasInteracted = false;
['click', 'scroll', 'touchstart'].forEach(event => {
window.addEventListener(event, () => {
if (!hasInteracted) {
hasInteracted = true;
loadThirdPartyScripts();
}
}, { once: true, passive: true });
});
function loadThirdPartyScripts() {
// Load chat widget
const script = document.createElement('script');
script.src = 'https://chat.example.com/widget.js';
document.body.appendChild(script);
}
</script>
<!-- 3. Use facade for embeds -->
<div class="youtube-facade" data-video-id="VIDEO_ID">
<img src="thumbnail.jpg" alt="Video thumbnail" loading="lazy">
<button>Play</button>
</div>
Progressive Enhancement
// Feature detection and progressive enhancement
// Base experience works without JavaScript
// Enhance for capable browsers
// Check for API support
if ('IntersectionObserver' in window) {
// Use intersection observer for lazy loading
lazyLoadWithIntersectionObserver();
} else {
// Fallback to scroll events
lazyLoadWithScroll();
}
// Progressive image loading
if ('loading' in HTMLImageElement.prototype) {
// Native lazy loading
images.forEach(img => img.loading = 'lazy');
} else {
// JavaScript lazy loading fallback
loadLazyLoadingPolyfill();
}
// Network-aware loading
if ('connection' in navigator && navigator.connection.saveData) {
// User has data saver enabled
document.documentElement.classList.add('save-data');
// Skip non-essential resources
}
Remember: Performance is not a one-time task but an ongoing process. Monitor, measure, and iterate. Use tools like Lighthouse CI to catch regressions in your build pipeline.