Web Performance Optimization: Beyond the Basics
Web performance is no longer just about speed—it's about user experience, SEO rankings, and business success. Let's dive into advanced optimization techniques that go beyond the basics.
Core Web Vitals Deep Dive
Largest Contentful Paint (LCP)
LCP measures when the largest content element becomes visible:
// Monitor LCP in production
new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
const lastEntry = entries[entries.length - 1];
console.log('LCP:', lastEntry.startTime);
// Send to analytics
analytics.track('LCP', { value: lastEntry.startTime });
}).observe({ entryTypes: ['largest-contentful-paint'] });
Optimization Strategies:
- Preload Critical Resources
<link rel="preload" href="/hero-image.jpg" as="image">
<link rel="preload" href="/critical-font.woff2" as="font" crossorigin>
- Optimize Images
// Responsive images with art direction
<picture>
<source media="(max-width: 768px)" srcset="mobile-image.jpg">
<source media="(min-width: 769px)" srcset="desktop-image.jpg">
<img src="fallback-image.jpg" alt="Hero image" loading="lazy">
</picture>
- Server-Side Rendering
// Next.js example with streaming
export async function getServerSideProps(context) {
const data = await fetchData();
return {
props: { data },
};
}
First Input Delay (FID)
FID measures responsiveness to user interactions:
// Measure FID
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
console.log('FID:', entry.processingStart - entry.startTime);
analytics.track('FID', { value: entry.processingStart - entry.startTime });
}
}).observe({ entryTypes: ['first-input'] });
Optimization Strategies:
- Code Splitting
// Dynamic imports for heavy components
const HeavyComponent = React.lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<Loading />}>
<HeavyComponent />
</Suspense>
);
}
- Web Workers
// Offload heavy computations
const worker = new Worker('/compute-worker.js');
worker.postMessage({ data: heavyData });
worker.onmessage = (e) => {
setResult(e.data);
};
Cumulative Layout Shift (CLS)
CLS measures visual stability:
// Monitor CLS
let clsValue = 0;
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
if (!entry.hadRecentInput) {
clsValue += entry.value;
}
}
console.log('CLS:', clsValue);
}).observe({ entryTypes: ['layout-shift'] });
Optimization Strategies:
- Reserve Space for Dynamic Content
/* Reserve space for images */
.image-container {
aspect-ratio: 16/9;
background-color: #f0f0f0;
}
/* Reserve space for ads */
.ad-placeholder {
min-height: 250px;
background-color: #f8f8f8;
}
- Font Display Strategy
@font-face {
font-family: 'Custom Font';
src: url('font.woff2') format('woff2');
font-display: swap; /* Prevents layout shift */
}
Advanced Caching Strategies
Service Worker Caching
// Service worker for offline-first experience
const CACHE_NAME = 'v1';
const urlsToCache = [
'/',
'/styles/main.css',
'/scripts/main.js',
'/images/logo.png'
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => cache.addAll(urlsToCache))
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((response) => {
// Return cached version or fetch from network
return response || fetch(event.request);
})
);
});
HTTP/2 Server Push
# Nginx configuration for HTTP/2 push
location / {
http2_push /styles/main.css;
http2_push /scripts/main.js;
http2_push /images/hero.jpg;
try_files $uri $uri/ /index.html;
}
Browser Caching Headers
// Express.js example
app.use(express.static('public', {
maxAge: '1y',
etag: true,
lastModified: true,
setHeaders: (res, path) => {
if (path.endsWith('.html')) {
res.setHeader('Cache-Control', 'no-cache');
}
}
}));
Resource Optimization
Critical CSS Inlining
// Extract and inline critical CSS
const critical = require('critical');
critical.generate({
src: 'index.html',
dest: 'index-critical.html',
inline: true,
minify: true,
width: 1300,
height: 900
});
Image Optimization Pipeline
// Sharp for image optimization
const sharp = require('sharp');
async function optimizeImage(input, output) {
await sharp(input)
.resize(800, 600, { fit: 'inside' })
.webp({ quality: 80 })
.jpeg({ quality: 80 })
.toFile(output);
}
Bundle Optimization
// Webpack optimization
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
runtimeChunk: 'single',
},
};
Network Performance
Resource Hints
<!-- Preconnect for external domains -->
<link rel="preconnect" href="https://api.example.com">
<link rel="preconnect" href="https://cdn.example.com">
<!-- DNS prefetch for future navigation -->
<link rel="dns-prefetch" href="https://blog.example.com">
<!-- Prefetch likely next pages -->
<link rel="prefetch" href="/next-page.html">
HTTP/3 and QUIC
# Enable HTTP/3 in Nginx
listen 443 quic reuseport;
listen 443 ssl http2;
add_header Alt-Svc 'h3=":443"; ma=86400';
CDN Configuration
// Cloudflare Workers for edge optimization
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
const url = new URL(request.url);
// Serve from edge cache
const cache = caches.default;
const cacheKey = new Request(url.toString(), request);
let response = await cache.match(cacheKey);
if (!response) {
const originResponse = await fetch(request);
response = new Response(originResponse.body, {
status: originResponse.status,
statusText: originResponse.statusText,
headers: originResponse.headers,
});
// Cache for 1 hour
response.headers.set('Cache-Control', 'public, max-age=3600');
event.waitUntil(cache.put(cacheKey, response.clone()));
}
return response;
}
Performance Monitoring
Real User Monitoring (RUM)
// Custom RUM implementation
class PerformanceMonitor {
constructor() {
this.metrics = {};
this.init();
}
init() {
// Page load metrics
window.addEventListener('load', () => {
this.collectPageMetrics();
});
// User interaction metrics
this.observeInteractions();
}
collectPageMetrics() {
const navigation = performance.getEntriesByType('navigation')[0];
this.metrics = {
dns: navigation.domainLookupEnd - navigation.domainLookupStart,
tcp: navigation.connectEnd - navigation.connectStart,
ssl: navigation.secureConnectionStart > 0
? navigation.connectEnd - navigation.secureConnectionStart
: 0,
ttfb: navigation.responseStart - navigation.requestStart,
download: navigation.responseEnd - navigation.responseStart,
domParse: navigation.domContentLoadedEventStart - navigation.responseEnd,
domReady: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart,
loadComplete: navigation.loadEventEnd - navigation.loadEventStart,
};
this.sendMetrics();
}
observeInteractions() {
let startTime;
document.addEventListener('click', (event) => {
startTime = performance.now();
});
document.addEventListener('load', (event) => {
if (startTime) {
const interactionTime = performance.now() - startTime;
this.trackInteraction('click', interactionTime);
startTime = null;
}
}, true);
}
sendMetrics() {
// Send to analytics service
fetch('/api/performance', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.metrics),
});
}
}
new PerformanceMonitor();
Performance Budgets
// Webpack Bundle Analyzer
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false,
reportFilename: 'bundle-report.html',
}),
],
};
Advanced Techniques
Intersection Observer for Lazy Loading
// Advanced lazy loading with Intersection Observer
class LazyLoader {
constructor() {
this.observer = new IntersectionObserver(
this.handleIntersection.bind(this),
{ rootMargin: '50px' }
);
this.init();
}
init() {
document.querySelectorAll('[data-lazy]').forEach(el => {
this.observer.observe(el);
});
}
handleIntersection(entries) {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.loadElement(entry.target);
this.observer.unobserve(entry.target);
}
});
}
loadElement(element) {
const src = element.dataset.lazy;
if (element.tagName === 'IMG') {
element.src = src;
element.onload = () => element.classList.add('loaded');
} else {
// Handle other element types
element.style.backgroundImage = `url(${src})`;
}
}
}
new LazyLoader();
WebAssembly for Performance-Critical Code
// Load WebAssembly module for heavy computations
async function loadWasmModule() {
const module = await WebAssembly.instantiateStreaming(
fetch('/compute.wasm'),
{ env: { memory: new WebAssembly.Memory({ initial: 256 }) } }
);
return module.instance;
}
// Use WebAssembly for image processing
async function processImage(imageData) {
const wasm = await loadWasmModule();
const result = wasm.exports.process_image(imageData);
return result;
}
Testing Performance
Lighthouse CI
# .lighthouserc.js
module.exports = {
ci: {
collect: {
url: ['http://localhost:3000'],
numberOfRuns: 3,
},
assert: {
assertions: {
'categories:performance': ['warn', { minScore: 0.9 }],
'categories:accessibility': ['error', { minScore: 0.9 }],
},
},
upload: {
target: 'temporary-public-storage',
},
},
};
Performance Testing with Artillery
# artillery-config.yml
config:
target: 'http://localhost:3000'
phases:
- duration: 60
arrivalRate: 10
- duration: 120
arrivalRate: 20
- duration: 60
arrivalRate: 50
scenarios:
- name: "Load homepage"
weight: 70
flow:
- get:
url: "/"
- name: "API call"
weight: 30
flow:
- post:
url: "/api/data"
json:
query: "test"
Conclusion
Web performance optimization is an ongoing process that requires attention to detail, continuous monitoring, and a willingness to experiment. By implementing these advanced techniques, you can create experiences that are not just fast, but delightful to use.
Remember that performance is a feature—one that directly impacts user satisfaction, conversion rates, and business success. Invest in it wisely, measure everything, and always put the user experience first.