Google uses Core Web Vitals as a ranking factor. A slow site does not just frustrate users — it literally pushes you down in search results. Yet most developers treat performance as an afterthought, adding a lazy loading directive and calling it done.

This guide takes you from red Lighthouse scores to green with concrete, measurable optimizations for each Core Web Vital.

The Three Core Web Vitals

Metric What It Measures Good Needs Improvement Poor
LCP (Largest Contentful Paint) Loading speed — when the biggest visible element renders ≤ 2.5s 2.5s - 4.0s > 4.0s
INP (Interaction to Next Paint) Responsiveness — delay between user interaction and visual response ≤ 200ms 200ms - 500ms > 500ms
CLS (Cumulative Layout Shift) Visual stability — how much the page layout shifts unexpectedly ≤ 0.1 0.1 - 0.25 > 0.25

Fixing LCP: Make the Biggest Element Load Fast

LCP measures when the largest visible element (usually a hero image, heading, or video poster) finishes rendering. The most common LCP killers:

1. Optimize Images

<!-- BEFORE: Unoptimized hero image -->
<img src="hero.png" alt="Hero">
<!-- 2.4MB PNG, no sizing, blocks rendering -->

<!-- AFTER: Optimized with 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"
    loading="eager"
    fetchpriority="high"
    decoding="async"
  >
</picture>
<!-- 85KB AVIF, explicit dimensions, high priority -->

Key rules for LCP images:

  • Use AVIF/WebP: 50-90% smaller than PNG/JPEG with equal quality
  • Set explicit width/height: Prevents layout shift and helps the browser allocate space early
  • Use fetchpriority="high": Tells the browser to prioritize this image over others
  • Never lazy-load the LCP image: Use loading="eager" (or omit the attribute) for above-the-fold images

2. Preload Critical Resources

<!-- In <head>: preload the LCP image -->
<link rel="preload" as="image" href="hero.avif" type="image/avif">

<!-- Preload critical fonts -->
<link rel="preload" as="font" href="/fonts/inter.woff2"
      type="font/woff2" crossorigin>

<!-- Preconnect to third-party origins -->
<link rel="preconnect" href="https://cdn.example.com">
<link rel="dns-prefetch" href="https://analytics.example.com">

3. Eliminate Render-Blocking Resources

<!-- BEFORE: Render-blocking CSS and JS -->
<link rel="stylesheet" href="all-styles.css">  <!-- 250KB -->
<script src="analytics.js"></script>          <!-- Blocks parsing -->

<!-- AFTER: Critical CSS inline, rest deferred -->
<style>
  /* Only above-the-fold critical CSS (~14KB) */
  .hero { ... }
  .nav { ... }
</style>
<link rel="stylesheet" href="full-styles.css" media="print"
      onload="this.media='all'">
<script src="analytics.js" defer></script>

Fixing INP: Make Interactions Feel Instant

INP replaced FID (First Input Delay) in March 2024. It measures the delay for all interactions, not just the first one. Common INP killers:

1. Break Up Long Tasks

// BEFORE: One long task blocks the main thread for 400ms
function processLargeList(items) {
  items.forEach(item => {
    expensiveCalculation(item);  // Blocks UI for entire loop
  });
}

// AFTER: Yield to the browser between chunks
async function processLargeList(items) {
  const CHUNK_SIZE = 50;

  for (let i = 0; i < items.length; i += CHUNK_SIZE) {
    const chunk = items.slice(i, i + CHUNK_SIZE);
    chunk.forEach(item => expensiveCalculation(item));

    // Yield to browser: allow input handling and rendering
    await new Promise(resolve => setTimeout(resolve, 0));
  }
}

2. Debounce Expensive Event Handlers

// BEFORE: Recalculates on every scroll event (60+ times/second)
window.addEventListener('scroll', () => {
  recalculateLayout();  // 50ms each = constant jank
});

// AFTER: Throttle to once per frame
let ticking = false;
window.addEventListener('scroll', () => {
  if (!ticking) {
    requestAnimationFrame(() => {
      recalculateLayout();
      ticking = false;
    });
    ticking = true;
  }
});

3. Use CSS contain for Complex Layouts

/* Tell the browser this element's layout is independent */
.card {
  contain: layout style paint;
  /* Browser skips recalculating this subtree when
     other parts of the page change */
}

/* For virtualized lists: contain size too */
.virtual-list-item {
  contain: strict;
  /* Browser can skip layout entirely for off-screen items */
}

Fixing CLS: Stop the Page from Jumping

1. Always Set Image Dimensions

<!-- BEFORE: No dimensions = layout shift when image loads -->
<img src="photo.jpg" alt="Photo">

<!-- AFTER: Browser reserves space before image loads -->
<img src="photo.jpg" alt="Photo" width="800" height="600">

<!-- Or use CSS aspect-ratio for responsive images -->
<style>
  .responsive-img {
    width: 100%;
    aspect-ratio: 16 / 9;
    object-fit: cover;
  }
</style>

2. Font Loading Strategy

/* BEFORE: FOUT (Flash of Unstyled Text) causes layout shift */
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter.woff2') format('woff2');
  /* Default: font-display: auto (browser decides) */
}

/* AFTER: Swap with size-adjust to minimize shift */
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter.woff2') format('woff2');
  font-display: swap;
  size-adjust: 107%;           /* Match fallback font metrics */
  ascent-override: 90%;
  descent-override: 22%;
  line-gap-override: 0%;
}

3. Reserve Space for Dynamic Content

<!-- BEFORE: Ad loads and pushes content down -->
<div id="ad-slot"></div>

<!-- AFTER: Pre-allocate space -->
<div id="ad-slot" style="min-height: 250px;"></div>

<!-- For skeleton loaders -->
<div class="skeleton" style="height: 200px; background: #e0e0e0;">
  <!-- Content loads here without shifting layout -->
</div>

Angular-Specific Optimizations

// 1. Use OnPush change detection
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  // Reduces unnecessary re-renders by 60-80%
})

// 2. Lazy load routes (already standard)
export const routes: Routes = [
  {
    path: 'dashboard',
    loadComponent: () => import('./dashboard.component')
  }
];

// 3. Use @defer for below-the-fold components
// In template:
// @defer (on viewport) {
//   <app-heavy-chart />
// } @placeholder {
//   <div class="skeleton" style="height: 400px"></div>
// }

// 4. Use NgOptimizedImage
import { NgOptimizedImage } from '@angular/common';

// <img ngSrc="hero.jpg" width="1200" height="600" priority />
// Automatically: sets fetchpriority, generates srcset,
// warns about missing dimensions, lazy loads by default

Measurement Tools

  • Lighthouse (Chrome DevTools): Lab data — synthetic tests on your machine
  • PageSpeed Insights: Combines lab data with real-user data from CrUX
  • Web Vitals JS library: Measure real user metrics in production
  • Chrome DevTools Performance tab: Flame chart showing exactly where time is spent
  • Search Console Core Web Vitals report: Aggregated field data from real users
// Measure real user Core Web Vitals
import { onLCP, onINP, onCLS } from 'web-vitals';

onLCP(metric => sendToAnalytics('LCP', metric));
onINP(metric => sendToAnalytics('INP', metric));
onCLS(metric => sendToAnalytics('CLS', metric));

function sendToAnalytics(name, metric) {
  fetch('/api/vitals', {
    method: 'POST',
    body: JSON.stringify({
      name,
      value: metric.value,
      rating: metric.rating,  // 'good', 'needs-improvement', 'poor'
      url: location.href,
    }),
    keepalive: true,
  });
}

Key Takeaways

  • LCP: Optimize the hero image (AVIF/WebP, preload, fetchpriority), inline critical CSS, defer everything else
  • INP: Break long tasks into chunks with setTimeout(0), throttle event handlers, use CSS contain
  • CLS: Always set image dimensions, use font-display: swap with size-adjust, reserve space for dynamic content
  • Never lazy-load above-the-fold content — it makes LCP worse, not better
  • Use Angular @defer for below-the-fold components — it is built-in code splitting
  • Measure real users, not just Lighthouse — lab scores and field data often tell different stories
  • Performance is a feature — every 100ms of delay reduces conversions by 7%

Web performance is not about chasing a perfect Lighthouse score. It is about ensuring real users on real devices have a fast, stable, responsive experience. Fix the fundamentals in this guide, measure with real-user data, and iterate on what matters most to your users.