Web Performance That Actually Matters — A Practical Guide to Core Web Vitals

May 30, 2026 (1mo ago)

Hey! Let's talk about web performance — not the theoretical kind, but the stuff that actually moves the needle for your users and your SEO rankings.

Google's Core Web Vitals have become the de facto standard for measuring web performance. But instead of just memorizing abbreviations, let's understand what they measure, why they matter, and how to fix them.

The Three Core Web Vitals

LCP (Largest Contentful Paint) — How fast does the main content appear?

INP (Interaction to Next Paint) — How responsive is the page?

CLS (Cumulative Layout Shift) — Does the page jump around?

Measuring Core Web Vitals

Lab tools (synthetic, controlled environment):

Field tools (real user data — this is what Google actually uses for ranking):

import { onLCP, onINP, onCLS } from 'web-vitals';
 
onLCP(metric => sendToAnalytics('LCP', metric.value));
onINP(metric => sendToAnalytics('INP', metric.value));
onCLS(metric => sendToAnalytics('CLS', metric.value));

Fixing LCP

LCP is usually about one thing: how fast can you render the biggest element on screen.

1. Optimize images (the #1 LCP culprit):

<!-- Use modern formats -->
<picture>
  <source srcset="hero.avif" type="image/avif" />
  <source srcset="hero.webp" type="image/webp" />
  <img src="hero.jpg" alt="Hero" width="1200" height="600"
       fetchpriority="high" decoding="async" />
</picture>

2. Preload critical resources:

<link rel="preload" as="image" href="/hero.avif" />
<link rel="preload" as="font" href="/Inter.woff2"
      type="font/woff2" crossorigin />

3. Inline critical CSS:

<head>
  <style>
    /* Inline only above-the-fold critical styles */
    .hero { background: #1a1a1a; min-height: 60vh; }
    .nav { position: fixed; top: 0; }
  </style>
  <!-- Load the rest asynchronously -->
  <link rel="preload" href="/styles.css" as="style"
        onload="this.onload=null;this.rel='stylesheet'" />
</head>

4. Reduce server response time (TTFB):

Fixing INP

INP is about main thread responsiveness. If your JavaScript is hogging the main thread, user interactions feel sluggish.

1. Break up long tasks:

// ❌ One long task (200ms) — blocks the main thread
function processItems(items) {
  items.forEach(item => heavyComputation(item));
}
 
// ✅ Yield to the main thread between chunks
async function processItems(items) {
  for (let i = 0; i < items.length; i += 50) {
    const chunk = items.slice(i, i + 50);
    chunk.forEach(item => heavyComputation(item));
    // Yield to the browser to handle user input
    await scheduler.yield(); // or setTimeout(0)
  }
}

2. Debounce expensive event handlers:

// ❌ Running expensive logic on every keystroke
input.addEventListener('input', (e) => {
  filterAndRenderResults(e.target.value); // 50ms per keystroke
});
 
// ✅ Debounce — wait for the user to stop typing
let timeout;
input.addEventListener('input', (e) => {
  clearTimeout(timeout);
  timeout = setTimeout(() => {
    filterAndRenderResults(e.target.value);
  }, 150);
});

3. Use startTransition in React:

import { startTransition } from 'react';
 
function SearchPage() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
 
  function handleInput(e) {
    setQuery(e.target.value); // Urgent — update input immediately
    startTransition(() => {
      setResults(search(e.target.value)); // Non-urgent — can be interrupted
    });
  }
}

Fixing CLS

CLS is about visual stability. The fix is usually simple: always reserve space for dynamic content.

1. Set explicit dimensions on images and videos:

<!-- ✅ Browser knows the size before loading -->
<img src="photo.jpg" width="800" height="600" alt="Photo" />
 
<!-- Or use CSS aspect-ratio -->
<style>
  .video-container {
    aspect-ratio: 16 / 9;
    width: 100%;
  }
</style>

2. Use font-display: swap carefully:

@font-face {
  font-family: 'Inter';
  src: url('/Inter.woff2') format('woff2');
  font-display: swap; /* Shows fallback font immediately, swaps when loaded */
}
 
/* Size-adjust the fallback to match the custom font */
@font-face {
  font-family: 'Inter-fallback';
  src: local('Arial');
  size-adjust: 107%; /* Reduces shift when fonts swap */
}

3. Don't inject content above existing content:

// ❌ Inserting a banner that pushes everything down
document.body.prepend(announcementBanner);
 
// ✅ Reserve space with a placeholder or use transform
// Animations using transform don't cause layout shifts
banner.style.transform = 'translateY(-100%)'; // Hidden
banner.style.transform = 'translateY(0)';     // Slides in, no shift

Code Splitting — Load Only What You Need

// ❌ Importing everything upfront
import { Chart } from 'chart.js'; // 200KB loaded on page load
 
// ✅ Dynamic import — load when needed
const ChartButton = () => {
  async function showChart() {
    const { Chart } = await import('chart.js');
    new Chart(canvas, config);
  }
  return <button onClick={showChart}>Show Chart</button>;
};

The Bottom Line

  1. Measure with field data, not just Lighthouse scores. Real users on slow phones matter more than your MacBook Pro.
  2. LCP: Optimize your hero image and reduce server response time.
  3. INP: Keep the main thread free — break long tasks, debounce handlers.
  4. CLS: Reserve space for everything before it loads.
  5. Performance is a feature. A site that loads in 1 second converts 2x better than one that loads in 5 seconds.

Start with the biggest bottleneck, fix it, measure again. Repeat.

See you in the next one!