Web development

Core Web Vitals in 2026: practical guide for developers

David Morales VegaJanuary 5, 202613 min read

Originally published on Bemorex Learn, our learning platform. · Leer en español

What are Core Web Vitals?

Core Web Vitals are a set of metrics defined by Google that measure the real user experience on a web page. In 2026 the three main metrics are:

  1. LCP (Largest Contentful Paint): visual loading speed
  2. INP (Interaction to Next Paint): responsiveness
  3. CLS (Cumulative Layout Shift): visual stability

These metrics are ranking factors in Google and directly affect user conversion and retention.

LCP: Largest Contentful Paint

LCP measures how long it takes to render the largest visible element in the viewport. It's usually a hero image, a video, or a large text block.

Thresholds

Classification Time
Good <= 2.5 seconds
Needs improvement <= 4.0 seconds
Poor > 4.0 seconds

Strategies to optimize LCP

1. Optimize the hero image

<!-- Use a modern format with a fallback -->
<picture>
  <source srcset="/hero.avif" type="image/avif" />
  <source srcset="/hero.webp" type="image/webp" />
  <img src="/hero.jpg" alt="Main banner"
       width="1200" height="600"
       fetchpriority="high"
       loading="eager" />
</picture>

Key points:

  • Use fetchpriority="high" on the LCP image
  • Never use loading="lazy" on the LCP image
  • Prefer AVIF or WebP formats
  • Always specify width and height

2. Preload critical resources

<head>
  <!-- Preload the LCP image -->
  <link rel="preload" as="image" href="/hero.webp"
        type="image/webp" fetchpriority="high" />

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

3. Eliminate render-blocking CSS and JS

<!-- Inline critical CSS -->
<style>
  /* Only above-the-fold styles: header, hero, nav */
  :root { --color-bg: #0a0a0f; }
  .hero { min-height: 60vh; display: grid; place-items: center; }
</style>

<!-- Non-critical CSS loaded asynchronously -->
<link rel="stylesheet" href="/styles.css" media="print"
      onload="this.media='all'" />

<!-- Deferred JavaScript -->
<script src="/app.js" defer></script>

4. Server-Side Rendering (SSR)

If you use an SPA framework like Angular, implement SSR so the HTML arrives pre-rendered:

Without SSR: Empty HTML -> Download JS -> Execute JS -> Render -> LCP
With SSR:    Complete HTML -> LCP (while JS downloads in parallel)

INP: Interaction to Next Paint

INP replaced FID (First Input Delay) in March 2024. It measures the total time from when the user interacts (click, tap, keystroke) until the browser paints the next frame.

Thresholds

Classification Time
Good <= 200 ms
Needs improvement <= 500 ms
Poor > 500 ms

Anatomy of an interaction

INP = Input Delay + Processing Time + Presentation Delay

1. Input Delay: time waiting for the main thread to be free
2. Processing Time: time executing event handlers
3. Presentation Delay: time calculating layout, paint, and composite

Strategies to optimize INP

1. Break up long tasks

// BAD: long task that blocks the main thread
function processData(items) {
  for (const item of items) {
    // expensive operation for each item
    transform(item);
  }
}

// GOOD: split into chunks, yielding to the browser
async function processData(items) {
  const CHUNK_SIZE = 50;

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

    for (const item of chunk) {
      transform(item);
    }

    // Yield control to the browser between chunks
    await scheduler.yield();
  }
}

2. Use requestIdleCallback for non-urgent work

function trackEvent(data) {
  // Do not block user interaction
  requestIdleCallback(() => {
    analytics.send(data);
  });
}

3. Virtualize long lists

Instead of rendering 10,000 elements in the DOM, use virtualization to render only the visible ones. In Angular, you can use @angular/cdk/scrolling:

import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';

// Only renders the visible items + buffer

CLS: Cumulative Layout Shift

CLS measures the sum of all unexpected layout changes during the page's lifetime.

Thresholds

Classification Score
Good <= 0.1
Needs improvement <= 0.25
Poor > 0.25

Common causes of CLS and solutions

1. Images without dimensions

<!-- BAD: causes layout shift when it loads -->
<img src="photo.jpg" alt="Photo" />

<!-- GOOD: reserves space with width and height -->
<img src="photo.jpg" alt="Photo" width="800" height="450" />

<!-- GOOD: use CSS aspect-ratio -->
<img src="photo.jpg" alt="Photo" style="aspect-ratio: 16/9; width: 100%;" />

2. Web fonts causing FOUT

/* Use font-display: swap with similar sizes */
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter.woff2') format('woff2');
  font-display: swap;
  /* Adjust metrics to minimize the shift */
  size-adjust: 100%;
  ascent-override: 90%;
  descent-override: 20%;
  line-gap-override: 0%;
}

3. Dynamically injected content

/* Reserve space for banners or ads */
.ad-slot {
  min-height: 250px;
  width: 100%;
  contain: layout;
}

4. Animations that cause layout

/* BAD: top/left trigger layout */
.menu {
  position: absolute;
  top: 0;
  transition: top 0.3s;
}

/* GOOD: transform does not trigger layout */
.menu {
  position: absolute;
  transform: translateY(0);
  transition: transform 0.3s;
}

Measurement tools

Field data (Real User Monitoring)

  • Chrome UX Report (CrUX): Real aggregated data from millions of Chrome users
  • web-vitals library: JavaScript library to measure in your own users
import { onLCP, onINP, onCLS } from 'web-vitals';

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

Lab data

  • Lighthouse: Automated audits in Chrome DevTools
  • WebPageTest: Detailed testing under different network conditions
  • Chrome DevTools Performance panel: Frame-by-frame analysis

Web performance checklist

  • [ ] The LCP image uses fetchpriority="high" and a modern format
  • [ ] Critical CSS is inlined in the <head>
  • [ ] Non-critical JavaScript uses defer or async
  • [ ] All images have width and height defined
  • [ ] Web fonts use font-display: swap with preload
  • [ ] JS tasks longer than 50ms are split into chunks
  • [ ] Animations use transform and opacity, not layout properties
  • [ ] Third-party resources use preconnect or dns-prefetch

Conclusion

Optimizing Core Web Vitals is not just an SEO concern: it's about building an experience that respects your users' time and attention. With LCP, INP, and CLS as guides, you have concrete, actionable metrics. Measure, optimize, measure again. Performance is a continuous process, not a destination.

Code examples

JavaScript

// Measure LCP with PerformanceObserver
const lcpObserver = new PerformanceObserver((list) => {
  const entries = list.getEntries();
  const lastEntry = entries[entries.length - 1];

  console.log('LCP:', lastEntry.startTime, 'ms');
  console.log('Element:', lastEntry.element);
});

lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });

// Measure INP
const inpObserver = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.interactionId) {
      console.log('Interaction:', entry.name);
      console.log('Duration:', entry.duration, 'ms');
    }
  }
});

inpObserver.observe({ type: 'event', buffered: true });

HTML

<!-- Preload the LCP image -->
<link rel="preload" as="image" href="/hero.webp"
      fetchpriority="high" />

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

<!-- Inline critical CSS -->
<style>
  /* Only the above-the-fold styles */
  .hero { min-height: 60vh; }
</style>

<!-- Load non-critical CSS asynchronously -->
<link rel="stylesheet" href="/styles.css" media="print"
      onload="this.media='all'" />

Keep reading

Want to build something like this?

Tell us what you need: we look at it together and propose a way forward in writing, in English or Spanish.

[email protected]