Performance 22 min read UPDATED: October 12, 2026

Core Web Vitals 2026:
LCP, INP & CLS Thresholds

Senorit

Summary

Core Web Vitals 2026 are Google's official performance metrics. INP replaced FID in March 2024, LCP measures loading time and CLS measures layout stability. Google judges each metric at the 75th percentile of real page loads. This guide shows how to fix each metric, with code examples and a checklist.

  • Google judges each metric at the 75th percentile of real page loads
  • INP (Interaction to Next Paint) replaced FID. It is stricter and measures all interactions
  • Target values: INP ≤200ms, LCP ≤2.5s, CLS ≤0.1
  • Field Data (CrUX) counts for rankings, not Lab Data
  • 28-30 days until improvements are visible in Search Console

About the author

Ebrahim Seyfi

Ebrahim Seyfi

Founder & Developer at Senorit, Full-Stack Developer since 2020

Published: January 12, 2026 Updated: October 12, 2026

Ebrahim Seyfi founded Senorit in Hamburg. He designs and builds websites and other digital projects for businesses in the DACH region. As a full-stack developer, he works with React, Next.js, Astro and TypeScript.

Expertise in

  • Core Web Vitals
  • INP
  • LCP
  • CLS
  • Performance
  • PageSpeed
  • Full-stack web development
  • Core Web Vitals
  • WCAG 2.2 accessibility
  • React, Astro & TypeScript

Only 48% of mobile websites reach good Core Web Vitals, according to the HTTP Archive Web Almanac 2025. INP (Interaction to Next Paint), which replaced FID in March 2024, is stricter than FID was, because it counts every interaction and not just the first one. This guide explains each of the three Core Web Vitals and shows how to measure and fix them, with working code examples and a checklist you can start on today.

p75
Field Data Percentile
200ms
INP Threshold
2.5s
LCP Target
0.1
CLS Maximum

What are Core Web Vitals?

Core Web Vitals are Google's official metrics for user experience on a website. There are three of them:

INP

Interaction to Next Paint: measures how quickly the page responds to user interactions

Good: ≤ 200ms

LCP

Largest Contentful Paint: measures how long the largest visible element takes to load

Good: ≤ 2.5s

CLS

Cumulative Layout Shift: measures unexpected layout shifts

Good: ≤ 0.1

Why Core Web Vitals matter

  • Core Web Vitals have been an official Google ranking factor since 2021.
  • Poor scores go along with a higher bounce rate and fewer conversions.
  • Google's evaluation is mobile-first, so mobile performance counts most.
  • If your content is as good as a competitor's, the faster page has the edge.
Metric Good Needs Improvement Poor
INP ≤ 200ms 200-500ms > 500ms
LCP ≤ 2.5s 2.5-4.0s > 4.0s
CLS ≤ 0.1 0.1-0.25 > 0.25

INP: the new metric since March 2024

The 2024 change

On March 12, 2024, Google made INP (Interaction to Next Paint) an official Core Web Vital, replacing FID (First Input Delay). INP is much stricter: it measures interactivity across the whole visit, where FID only looked at the first input.

What INP measures

INP measures the time from a user interaction (click, tap, keystroke) to the next visual feedback on screen. FID only measured the first interaction. INP records all interactions for as long as the visitor stays on the page.

FID (deprecated)

  • Measured only the first interaction
  • Input Delay only, not Processing + Paint
  • Easy to optimize, but it told you little
  • Showed only part of the user experience

INP (current)

  • Observes all interactions and reports the slowest, ignoring outliers
  • Complete cycle: Input + Processing + Paint
  • Reflects what real users experience
  • Gives a more realistic picture of interactivity

INP optimization techniques

1. Break up JavaScript tasks

Long JavaScript tasks block the main thread. Split large functions into smaller chunks.

// BAD: One long task blocks the main thread
function processLargeArray(items) {
  items.forEach(item => heavyComputation(item));
}

// GOOD: Break tasks with requestIdleCallback
function processLargeArrayOptimized(items) {
  let index = 0;

  function processChunk(deadline) {
    while (index < items.length && deadline.timeRemaining() > 0) {
      heavyComputation(items[index]);
      index++;
    }

    if (index < items.length) {
      requestIdleCallback(processChunk);
    }
  }

  requestIdleCallback(processChunk);
}

2. Optimize event listeners

Use passive event listeners and debounce/throttle for scroll/resize events.

// BAD: Blocking scroll handler
document.addEventListener('scroll', handleScroll);

// GOOD: Passive listener + Throttling
document.addEventListener('scroll', throttle(handleScroll, 100), { passive: true });

function throttle(func, limit) {
  let inThrottle;
  return function(...args) {
    if (!inThrottle) {
      func.apply(this, args);
      inThrottle = true;
      setTimeout(() => inThrottle = false, limit);
    }
  }
}

3. Defer third-party scripts

Load analytics, chat widgets and ads only after the first interaction or after a delay.

// Load only when user interacts or after 5 seconds
const loadThirdParty = () => {
  if (window.thirdPartyLoaded) return;
  window.thirdPartyLoaded = true;

  const script = document.createElement('script');
  script.src = 'https://third-party.example.com/widget.js';
  document.head.appendChild(script);
};

// On first interaction
['mousedown', 'touchstart', 'keydown'].forEach(event => {
  document.addEventListener(event, loadThirdParty, { once: true });
});

// Or after timeout
setTimeout(loadThirdParty, 5000);

LCP: optimizing Largest Contentful Paint

LCP measures how long it takes for the largest visible element in the viewport to render. Usually that is a hero image, a large heading or a video poster.

Elements that can count as LCP:

  • • <img> images
  • • <image> inside SVG
  • • <video> poster images
  • • Elements with background-image
  • • Block-level text elements (h1, p, etc.)
  • • <svg> graphics

The 5 most important LCP optimizations

1. Preload the LCP element

Load the LCP element as early as possible with a preload hint.

<!-- In <head> - BEFORE other resources -->
<link
  rel="preload"
  as="image"
  href="/hero-image.webp"
  fetchpriority="high"
/>

<!-- For responsive images -->
<link
  rel="preload"
  as="image"
  href="/hero-mobile.webp"
  media="(max-width: 768px)"
  fetchpriority="high"
/>

2. Use optimized image formats

Use modern image formats and responsive images with srcset.

<picture>
  <!-- AVIF: Best compression, limited support -->
  <source
    type="image/avif"
    srcset="
      /hero-400.avif 400w,
      /hero-800.avif 800w,
      /hero-1200.avif 1200w
    "
    sizes="(max-width: 768px) 100vw, 50vw"
  />
  <!-- WebP: Good compression, broad support -->
  <source
    type="image/webp"
    srcset="
      /hero-400.webp 400w,
      /hero-800.webp 800w,
      /hero-1200.webp 1200w
    "
    sizes="(max-width: 768px) 100vw, 50vw"
  />
  <!-- Fallback for older browsers -->
  <img
    src="/hero-800.jpg"
    alt="Hero"
    width="1200"
    height="600"
    loading="eager"
    fetchpriority="high"
    decoding="async"
  />
</picture>

3. Optimize server response time (TTFB)

A slow Time to First Byte delays LCP. Speed up your backend and hosting.

  • • Use a CDN such as Cloudflare, Vercel Edge or AWS CloudFront
  • • Cache on the server with Redis, Varnish or edge caching
  • • Optimize the database with indexes and faster queries
  • • Use HTTP/2 or HTTP/3 to multiplex parallel requests
  • • Generate pages statically (Astro, Next.js SSG) instead of using SSR

4. Eliminate render-blocking resources

CSS and JavaScript can block rendering. Optimize the loading order.

<!-- Critical CSS inline -->
<style>
  /* Only necessary CSS for Above-the-Fold */
  .hero { ... }
  .nav { ... }
</style>

<!-- Load rest of CSS asynchronously -->
<link
  rel="preload"
  href="/styles.css"
  as="style"
  onload="this.onload=null;this.rel='stylesheet'"
/>
<noscript><link rel="stylesheet" href="/styles.css"></noscript>

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

5. Optimize fonts

Web fonts can delay LCP when text is the LCP element.

<!-- Font preload with font-display: swap -->
<link
  rel="preload"
  href="/fonts/inter.woff2"
  as="font"
  type="font/woff2"
  crossorigin
/>

<style>
  @font-face {
    font-family: 'Inter';
    src: url('/fonts/inter.woff2') format('woff2');
    font-display: swap; /* Shows fallback font until loaded */
    font-weight: 400;
  }
</style>

CLS: understanding Cumulative Layout Shift

CLS measures unexpected shifts of elements on the page. A high CLS value makes a page frustrating to use. You will know the effect: you go to click a button and the whole layout jumps.

The most common CLS causes:

  • Images without width/height attributes
  • Ads, embeds, iFrames without reserved space
  • Dynamically inserted content
  • Web fonts causing FOIT/FOUT
  • Animations without transform
  • Cookie banners that shift layout
  • Lazy-loaded content without placeholder
  • Sticky headers that shift other elements

CLS optimization best practices

1. Always specify dimensions for images/videos

<!-- BAD: No dimensions -->
<img src="photo.jpg" alt="Photo">

<!-- GOOD: Explicit width/height -->
<img src="photo.jpg" alt="Photo" width="800" height="600">

<!-- ALSO GOOD: CSS aspect-ratio -->
<style>
  .responsive-image {
    width: 100%;
    height: auto;
    aspect-ratio: 16 / 9;
    object-fit: cover;
  }
</style>
<img src="photo.jpg" alt="Photo" class="responsive-image">

2. Placeholders for dynamic content

<!-- Reserved space for ads -->
<div class="ad-container" style="min-height: 250px;">
  <!-- Ad loads here -->
</div>

<!-- Skeleton for lazy-loaded content -->
<div class="card-skeleton" style="height: 300px;">
  <div class="skeleton-image" style="height: 180px;"></div>
  <div class="skeleton-text"></div>
</div>

/* CSS for Skeleton */
.skeleton-image {
  background: linear-gradient(90deg, #131824 25%, #1c2333 50%, #131824 75%);
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite;
}

@keyframes shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

3. Use transform instead of position for animations

/* BAD: Shifts the layout */
.modal-open {
  top: 100px; /* Causes Layout Shift */
  left: 50px;
}

/* GOOD: Uses GPU, no Layout Shift */
.modal-open {
  transform: translateY(100px) translateX(50px);
}

/* BAD: Changes dimensions */
.expanded {
  width: 200px;
  height: 300px;
}

/* GOOD: Scales without layout change */
.expanded {
  transform: scale(1.2);
}

4. Implement cookie banners correctly

/* BAD: Banner shifts content */
.cookie-banner {
  position: relative; /* Takes space in flow */
}

/* GOOD: Fixed or Sticky, no shift */
.cookie-banner {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 9999;
}

/* ALSO GOOD: Overlay instead of banner */
.cookie-overlay {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.8);
}

Measurement and monitoring

There are two types of Core Web Vitals data: Field Data (real user data) and Lab Data (simulated tests). For Google rankings, only Field Data counts.

Field Data (RUM)

Real user data from the Chrome User Experience Report (CrUX).

  • Google Search Console, in the Core Web Vitals report
  • PageSpeed Insights, in the "Field Data" section
  • CrUX Dashboard for historical data
  • web-vitals.js for your own RUM setup

Lab Data (Synthetic)

Simulated tests in controlled environments.

  • Lighthouse, built into Chrome DevTools
  • PageSpeed Insights, in the "Lab Data" section
  • WebPageTest for detailed waterfall analysis
  • Debugbear for continuous monitoring

Implementing web-vitals.js

// Installation: npm install web-vitals

import { onINP, onLCP, onCLS } from 'web-vitals';

// Send to analytics
function sendToAnalytics(metric) {
  const body = JSON.stringify({
    name: metric.name,
    value: metric.value,
    delta: metric.delta,
    id: metric.id,
    rating: metric.rating, // 'good', 'needs-improvement', or 'poor'
  });

  // Beacon API for reliable transmission
  if (navigator.sendBeacon) {
    navigator.sendBeacon('/analytics', body);
  } else {
    fetch('/analytics', { body, method: 'POST', keepalive: true });
  }
}

// Measure all Core Web Vitals
onINP(sendToAnalytics);
onLCP(sendToAnalytics);
onCLS(sendToAnalytics);

Tip: attribution for debugging

The attribution build tells you which element caused a slow interaction and where the time went:

import { onINP } from 'web-vitals/attribution';

onINP((metric) => {

});

Advanced optimization techniques

1

Priority hints for resources

With fetchpriority you can tell the browser which resources are important.

<!-- Hero image with high priority -->
<img src="hero.jpg" fetchpriority="high" alt="Hero">

<!-- Below-the-fold images with low priority -->
<img src="footer-logo.png" fetchpriority="low" loading="lazy" alt="Logo">

<!-- Scripts -->
&lt;script src="critical.js" fetchpriority="high"&gt;&lt;/script&gt;
&lt;script src="analytics.js" fetchpriority="low" defer&gt;&lt;/script&gt;
2

Speculation Rules API (Chromium browsers)

The browser prerenders likely next pages, so navigating to them is close to instant.

<script is:inline type="speculationrules">
{
  "prerender": [{
    "source": "list",
    "urls": ["/about", "/contact", "/pricing"]
  }],
  "prefetch": [{
    "source": "document",
    "where": {
      "href_matches": "/*"
    },
    "eagerness": "moderate"
  }]
}
</script>
3

View Transitions API

Animate page changes without causing layout shifts.

---
// src/layouts/Layout.astro (Astro 5 and later)
// <ClientRouter /> replaces the old <ViewTransitions /> component
import { ClientRouter } from 'astro:transitions';
---

<head>
  <ClientRouter />
</head>

<!-- Animated elements -->
<h1 transition:name="page-title">Page Title</h1>
<img transition:name="hero" src="/hero.jpg" alt="" />
4

Scheduler API for JavaScript

Prioritize JavaScript tasks for better INP values.

// High priority: User-visible updates
scheduler.postTask(() => {
  updateUI();
}, { priority: 'user-blocking' });

// Low priority: Analytics, logging
scheduler.postTask(() => {
  sendAnalytics();
}, { priority: 'background' });

// Yielding to Main Thread
async function processItems(items) {
  for (const item of items) {
    processItem(item);
    await scheduler.yield(); // Release main thread
  }
}

Complete code example: optimized hero section

The hero section below puts the LCP, CLS and INP techniques from this guide into one page:

<!-- HTML: Optimized Hero Section -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <!-- Preconnect for external resources -->
  <link rel="preconnect" href="https://cdn.example.com" crossorigin>

  <!-- Preload critical resources -->
  <link
    rel="preload"
    as="image"
    href="/hero-mobile.webp"
    media="(max-width: 768px)"
    fetchpriority="high"
  >
  <link
    rel="preload"
    as="image"
    href="/hero-desktop.webp"
    media="(min-width: 769px)"
    fetchpriority="high"
  >
  <link rel="preload" as="font" href="/fonts/inter.woff2" type="font/woff2" crossorigin>

  <!-- Critical CSS inline -->
  <style>
    /* Reset and Critical Styles */
    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

    @font-face {
      font-family: 'Inter';
      src: url('/fonts/inter.woff2') format('woff2');
      font-display: swap;
    }

    body {
      font-family: 'Inter', system-ui, sans-serif;
      background: #0a0d14;
      color: #e9edf3;
    }

    /* Hero with fixed dimensions for CLS */
    .hero {
      min-height: 100vh;
      display: flex;
      align-items: center;
      position: relative;
      overflow: hidden;
    }

    .hero-image {
      position: absolute;
      inset: 0;
      width: 100%;
      height: 100%;
      object-fit: cover;
    }

    .hero-content {
      position: relative;
      z-index: 1;
      max-width: 1200px;
      margin: 0 auto;
      padding: 2rem;
    }

    /* Animations with transform (no Layout Shift) */
    .hero-title {
      opacity: 0;
      transform: translateY(20px);
      animation: fadeInUp 0.6s ease-out 0.1s forwards;
    }

    @keyframes fadeInUp {
      to { opacity: 1; transform: translateY(0); }
    }

    /* Button with fixed size */
    .cta-button {
      display: inline-block;
      padding: 1rem 2rem;
      min-width: 200px;
      text-align: center;
      /* ... more styles */
    }
  </style>
</head>
<body>
  <section class="hero">
    <!-- Hero image with all optimizations -->
    <picture>
      <source
        type="image/webp"
        media="(max-width: 768px)"
        srcset="/hero-mobile.webp"
      >
      <source
        type="image/webp"
        media="(min-width: 769px)"
        srcset="/hero-desktop.webp"
      >
      <img
        src="/hero-desktop.jpg"
        alt="Hero Image"
        class="hero-image"
        width="1920"
        height="1080"
        fetchpriority="high"
        decoding="async"
      >
    </picture>

    <div class="hero-content">
      <h1 class="hero-title">Your Headline Here</h1>
      <a href="/contact" class="cta-button">Get Started</a>
    </div>
  </section>

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

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

Core Web Vitals checklist 2026

INP optimization

LCP optimization

CLS optimization

Frequently asked questions

How long until improvements are visible in Search Console?

Google uses a 28-day rolling average of CrUX data. After you ship an improvement, expect 28-30 days before Search Console shows the new values in full. Larger websites may see the change a little sooner because they collect more user data.

My Lab data is good, but my Field data is bad. Why?

Lab tests run under controlled conditions, while Field data comes from real visitors. Possible reasons for the gap: your users have slower devices or poor internet connections (3G/4G), third-party scripts load for them but not in the lab, or geography plays a part because your server is far from your users.

Does the 75th percentile apply to all metrics?

Yes. Google uses the 75th percentile of page loads in the field data for all three Core Web Vitals. At least 75% of recorded page visits must reach the "Good" threshold for the page to pass, which means visits on slower devices count too.

How do poor Core Web Vitals affect rankings?

Core Web Vitals work as a tiebreaker signal. Content still matters more than performance, but when two pages offer equivalent content, the faster one wins. Poor CWV can put you at a disadvantage against competitors with similar content, and in competitive niches that disadvantage can be significant.

Do I need good CWV for every URL or just the domain?

Google evaluates at the URL level. When a URL doesn't have enough data of its own, Google uses aggregated data from similar pages. If traffic is low, Google falls back to the origin level (the entire domain). Optimize each of your important pages on its own.

Where to go from here

Core Web Vitals are an SEO signal, and they also show you how your site feels to use. Fix the scores and you usually see results like these:

  • Higher Google rankings when content quality is comparable
  • Fewer visitors leaving before the page has loaded
  • Forms and checkouts that react without delay, which helps conversions
  • A better experience on mobile phones, where most slow-page problems show up
  • An edge over competitors whose pages still fail the thresholds

Start by measuring your current scores. Then tackle the biggest gap first (usually INP or LCP) and work through the checklist section by section. Most of the fixes take hours rather than weeks. If you would rather hand it off, our SEO agency in Hamburg includes Core Web Vitals in every engagement.

Need a Core Web Vitals audit?

We measure your site with field data and Lighthouse, find the bottlenecks and fix them, aiming for "Good" in all three Core Web Vitals. Start with a free initial consultation. You get a fixed-price quote afterwards.