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.