Web Performance
Web Performance Guide
Web Performance Cheat Sheet: Core Web Vitals Optimization
Quick reference for optimizing LCP, FID, and CLS: image optimization, font loading, JavaScript efficiency, and layout stability
Web Performance Cheat Sheet: Core Web Vitals Optimization
📊 Core Web Vitals Targets
The Three Metrics That Matter
| Metric |
Good |
Needs Improvement |
Poor |
| LCP (Largest Contentful Paint) |
< 2.5s |
2.5s - 4.0s |
> 4.0s |
| FID (First Input Delay) |
< 100ms |
100ms - 300ms |
> 300ms |
| CLS (Cumulative Layout Shift) |
< 0.1 |
0.1 - 0.25 |
> 0.25 |
Measure at 75th percentile of page loads, segmented by mobile and desktop
⚡ LCP Optimization (Loading Performance)
🚀 Critical Rendering Path
<!-- Preload hero image -->
<link rel="preload" as="image" href="/hero.avif" fetchpriority="high">
<!-- Preload critical fonts -->
<link rel="preload" as="font" href="/fonts/inter-bold.woff2" crossorigin>
<!-- Eliminate render-blocking CSS -->
<link rel="stylesheet" href="/critical.css" media="print" onload="this.media='all'">
<!-- Inline critical CSS -->
<style>
/* Above-the-fold styles */
.hero { background: url('/hero.avif') center/cover no-repeat; }
</style>
🖼️ Image Optimization
| Technique |
Impact |
Implementation |
| Modern formats |
50-80% smaller |
AVIF > WebP > JPEG/PNG |
| Proper sizing |
30-50% smaller |
Width/height match display size |
| Lazy loading |
Below-the-fold |
loading="lazy" on non-critical images |
| CDN delivery |
Global edge |
Serve from Cloudflare/AWS CloudFront |
| Compression |
10-20% smaller |
MozJPEG, Oxipng, SVGO |
🔧 Server Optimization
- HTTP/2 or HTTP/3: Multiplexing, header compression
- Cache-Control:
public, max-age=31536000, immutable for assets
- Brotli/Gzip compression: 70-80% text reduction
- Edge caching: Cloudflare/CloudFront for HTML/API
⚡ FID Optimization (Interactivity)
⚡ JavaScript Efficiency
// ✅ GOOD: Break up long tasks
function processLargeData() {
const chunkSize = 1000;
for (let i = 0; i < data.length; i += chunkSize) {
processChunk(data.slice(i, i + chunkSize));
// Yield to browser every 50ms
if (i % 5000 === 0) await yieldToBrowser();
}
}
// ✅ GOOD: Use web workers for CPU-intensive work
const worker = new Worker('data-processor.worker.js');
worker.postMessage(data);
worker.onmessage = (e) => handleResult(e.data);
// ✅ GOOD: RequestIdleCallback for low-priority work
requestIdleCallback(() => {
// Analytics, prefetching, non-critical updates
});
// ❌ BAD: Blocking the main thread
function expensiveOperation() {
// This blocks FID for seconds!
for (let i = 0; i < 1000000; i++) {
// Heavy computation
}
}
📦 Bundle Optimization
| Technique |
Tool |
Impact |
| Code splitting |
Webpack/Rollup/Vite |
Load only what's needed |
| Tree shaking |
ESBuild/SWC |
Remove unused exports |
| Dynamic import |
import() |
Load on demand |
| Prefabrication |
Astro islands |
Ship HTML, hydrate selectively |
⚡ Framework-Specific
- Astro: Zero-JS by default, client:* directives
- React: React.lazy(), Suspense, useTransition()
- Vue: defineAsyncComponent(), suspense
- Svelte: code splitting via dynamic imports
🎯 CLS Optimization (Visual Stability)
📏 Dimension Reservation
<!-- ✅ GOOD: Reserve space for images -->
<img src="/banner.avif" width="1200" height="400" alt="Banner">
<!-- ✅ GOOD: Reserve space for ads -->
<div class="ad-slot" style="min-height: 90px">
<!-- Ad loads here later -->
</div>
<!-- ❌ BAD: No dimensions causes layout shift -->
<img src="/banner.avif" alt="Banner"> <!-- Browser doesn't know size until loaded -->
🎬 Animation Best Practices
/* ✅ GOOD: Use transform for animations */
.animated-element {
transition: transform 0.3s ease;
/* Animates without layout recalculation */
}
/* ❌ BAD: Animating properties that trigger layout */
.animated-element {
transition: top 0.3s ease, left 0.3s ease; /* Triggers layout */
/* Causes layout shifts and jank */
}
/* ✅ GOOD: Use opacity for fade-ins */
.fade-in {
animation: fadeIn 0.5s ease-out;
/* Doesn't affect layout */
}
📋 Dynamic Content Injection
// ✅ GOOD: Reserve space first, then fill
const container = document.createElement('div');
container.style.minHeight = '200px'; // Reserve space
container.id = 'dynamic-content';
document.body.appendChild(container);
// Later, when content is ready:
document.getElementById('dynamic-content').innerHTML = content;
// ❌ BAD: Injecting without reservation
document.body.innerHTML += '<div>New content</div>'; // Pushes everything down
🔧 Font Loading Strategies
/* ✅ GOOD: Font loading strategy */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-regular.woff2') format('woff2');
font-weight: 400;
font-display: swap; /* Critical: prevents invisible text */
}
/* Preload critical fonts */
<link rel="preload" as="font" href="/fonts/inter-bold.woff2" crossorigin>
/* ❌ BAD: FOIT (Flash of Invisible Text) */
@font-face {
font-display: block; /* Makes text invisible until font loads */
}
📱 Mobile-Specific Optimization
⚡ Thumb-Friendly Interactions
- Minimum touch target: 48x48px
- Recommended: 56x56px with 8px spacing
- Place primary actions in thumb zone (bottom center)
📶 Network Awareness
// Adaptive quality based on connection
if (navigator.connection.saveData) {
// Serve low-resolution images, disable animations
} else if (navigator.connection.effectiveType === '2g') {
// Reduce payload, simplify interactions
}
// Save-Data header support
app.get('/image.jpg', (req, res) => {
if (req.headers['save-data']) {
res.sendFile('/low-res-image.jpg');
} else {
res.sendFile('/high-res-image.jpg');
}
});
🔧 Testing & Monitoring
🧪 Lab Testing Tools
- Lighthouse: Automated audits (CI/CD friendly)
- WebPageTest: Detailed waterfall, multiple connection speeds
- Chrome DevTools: Performance panel, throttling, screenshots
📈 Field Data Collection
- Chrome UX Report (CrUX): Real-user data from Chrome users
- Web Vitals JavaScript library: Collect in production
- Analytics integration: Send CWV to GA/GA4/custom endpoints
🚨 Performance Budgets
// .budget.json
{
"resourceSizes": [
{ "resourceType": "document", "budget": 50 },
{ "resourceType": "image", "budget": 1000 },
{ "resourceType": "script", "budget": 150 },
{ "resourceType": "stylesheet", "budget": 50 }
],
"resourceCounts": [
{ "resourceType": "third-party", "budget": 4 }
],
"timings": [
{ "metric": "LCP", "budget": 2500 },
{ "metric": "FID", "budget": 100 },
{ "metric": "CLS", "budget": 0.1 }
]
}
Keep this cheat sheet handy — the difference between "good" and "needs improvement" is often just 1-2 optimizations.