EmpireUI
Get Pro
← Blog9 min read#core web vitals#lcp#cls

Core Web Vitals in 2026: LCP, CLS, INP — Measure and Fix

LCP, CLS, INP — Google's 2026 thresholds are tighter. Here's how to measure every metric and actually fix the numbers that drag your ranking down.

colorful abstract speed lines representing web performance metrics dashboard

Why Core Web Vitals Still Matter in 2026

Google hasn't backed off. If anything, CWV scores carry more weight in 2026 than they did when the initiative launched back in 2021 — the Page Experience signal now feeds directly into the Helpful Content system, and bad INP can knock you out of featured snippets even if your content is otherwise stellar.

That said, a lot of dev teams still treat CWV as an ops problem. They dump it on the platform team, wait for a Lighthouse report, and call it done. That's a mistake. The fixes live in your component code — in how you load fonts, size images, handle animations, and wire up interaction handlers. You're the one who needs to care about this.

In practice, the three metrics you track are Largest Contentful Paint (LCP), Cumulative Layout Shift (CLS), and Interaction to Next Paint (INP). FID is dead — Google dropped it in March 2024 in favour of INP, which measures responsiveness across the entire page lifetime, not just first load. If you're still optimising for FID somewhere, stop.

The passing thresholds as of 2026: LCP ≤ 2500ms, INP ≤ 200ms, CLS ≤ 0.1. Miss any one of those on 75th-percentile field data and you're in the red. Chrome User Experience Report (CrUX) is the source of truth — not Lighthouse, not your local devtools.

Measuring: Field Data vs Lab Data

Here's where most teams go wrong. They run Lighthouse locally, hit green, ship — then wonder why Search Console shows a sea of red. Lighthouse is lab data. It runs on a simulated mid-tier Android device under throttled 4G, with zero real user variance. It's useful for catching regressions early, but it's not what Google's algorithm uses.

Field data comes from real users via the CrUX dataset and the web-vitals JavaScript library. You need both. Add the library to your app and start logging to an analytics endpoint — this is non-negotiable if you want to understand your 75th-percentile numbers before they show up in Search Console three weeks after the damage is done.

npm install web-vitals
```

```ts
// vitals.ts — drop this in your root layout
import { onLCP, onCLS, onINP } from 'web-vitals';

function sendToAnalytics(metric: any) {
  // replace with your actual endpoint
  navigator.sendBeacon('/api/vitals', JSON.stringify({
    name: metric.name,
    value: metric.value,
    rating: metric.rating, // 'good' | 'needs-improvement' | 'poor'
    id: metric.id,
    navigationType: metric.navigationType,
  }));
}

onLCP(sendToAnalytics);
onCLS(sendToAnalytics);
onINP(sendToAnalytics);

Worth noting: onINP replaced onFID in web-vitals v3. If you're on v2 or older, upgrade. The API is compatible but FID data is not the same signal.

Quick aside: PageSpeed Insights pulls from CrUX automatically if your URL has enough traffic. For lower-traffic pages or staging environments, you'll rely on lab data only — that's fine, just don't confuse one for the other.

LCP: Finding and Fixing the Slow Element

LCP measures how long it takes the largest visible content element to render. Usually that's a hero image or an H1 above the fold. Sometimes it's a background image pulled through CSS. The browser's LCP candidate can change as content loads, which makes it trickier to pin down than you'd expect.

Open Chrome DevTools → Performance tab → record a page load. Look for the LCP marker in the timings track. Click it and DevTools tells you exactly which element got flagged. Nine times out of ten it's an <img> that's loading lazily when it shouldn't be, or a font blocking text render, or a server that's just slow.

<!-- Wrong: hero image loading lazily -->
<img src="/hero.jpg" loading="lazy" />

<!-- Right: prioritise the LCP candidate -->
<img
  src="/hero.jpg"
  fetchpriority="high"
  decoding="async"
  width="1440"
  height="600"
  alt="Hero banner"
/>

Honestly, fetchpriority="high" is underused. Adding it to your hero image alone can shave 400–800ms off LCP on a real device — because the browser stops deprioritising it in the request queue. Pair it with a <link rel="preload"> in your <head> for the largest image and you'll see the difference in CrUX within 28 days.

For Next.js 15+ users, <Image priority /> handles the preload automatically. But if you're using a custom <img> or a third-party component library, you need to add fetchpriority yourself — Next.js won't do it for you.

CLS: Stopping Layout Jumps Before They Happen

CLS is the most annoying metric to debug because the shifts happen fast and they're often invisible until you use the right tool. In DevTools, open the Rendering panel and enable 'Layout Shift Regions' — every element that causes a shift gets highlighted in blue. Do it on a slow connection and you'll suddenly see things you had no idea were moving.

The usual suspects: images without explicit width/height, ads or embeds that inject at a fixed position, dynamically injected banners, and web fonts that swap at load. Each one has a clean fix.

/* Always reserve space for images */
img {
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9; /* or whatever matches your image */
}

/* Font display swap is better than block for CLS */
@font-face {
  font-family: 'MyFont';
  src: url('/fonts/myfont.woff2') format('woff2');
  font-display: swap;
}

Look, the aspect-ratio CSS property is the real hero here. Before it existed (pre-2021 browsers), you'd fake aspect ratios with the padding-top hack. You don't need that anymore. Set explicit dimensions on every image and embed, use aspect-ratio where dimensions vary, and your CLS will drop fast.

One more thing — if you're using glassmorphism components or any animated UI elements from Empire UI, make sure they don't animate top, left, margin, or padding properties during load. Those trigger layout. Animate transform and opacity instead — zero CLS impact because composited layers don't affect document flow.

INP: The Metric Most Teams Are Failing

INP replaced FID in 2024 and it's harder to pass. FID measured just the delay before your first event handler fires. INP measures the total time from user interaction to the next visual update — input delay + processing time + presentation delay — for every interaction on the page, then reports the worst one (minus outliers).

The 200ms threshold is strict. On a budget Android with a slow JS thread, you can blow past it just by running a synchronous filter over a 500-item array inside a click handler. Real-world INP failures usually come from: heavy main-thread work during scroll or click, synchronous fetch calls, React re-renders that touch huge trees, and third-party scripts that steal the thread mid-interaction.

// Bad: heavy synchronous work blocks the thread
function handleSearch(query: string) {
  const results = hugeList.filter(item =>
    item.name.toLowerCase().includes(query.toLowerCase())
  );
  setResults(results);
}

// Better: yield to the browser between chunks
async function handleSearch(query: string) {
  // Let the browser paint first
  await scheduler.yield();
  const results = hugeList.filter(item =>
    item.name.toLowerCase().includes(query.toLowerCase())
  );
  setResults(results);
}

scheduler.yield() landed in Chrome 115 and is now baseline. It's the cleanest way to break up long tasks. For broader support, fall back to setTimeout(fn, 0) wrapped in a Promise. The difference in INP scores when you start yielding before heavy filtering operations is dramatic — I've seen 50th percentile INP drop from 380ms to 140ms on a real e-commerce site just from this one change.

Worth noting: if you're building with React 19+, the new compiler does some automatic batching that reduces unnecessary re-renders. But it doesn't magically fix long tasks — you still need to move expensive work off the main thread or yield through it. Check your INP breakdown in the Performance panel's 'Interactions' track; it'll show exactly where the 200ms is going.

Tooling: What to Actually Run

You need three tools running at different stages. Locally during development: Lighthouse CI in your terminal or as a PR check. In staging: PageSpeed Insights against a public URL. In production: the web-vitals library logging to your analytics, plus Search Console for CrUX aggregates.

``bash # Install Lighthouse CI npm install -g @lhci/cli # Run against local build npx next build && npx next start & lhci autorun --collect.url=http://localhost:3000 `` For GitHub Actions, the Lighthouse CI GitHub App posts scores directly on PRs — if you haven't set that up yet, it takes about 20 minutes and saves a lot of 'but it passed locally' arguments.

The Chrome DevTools Performance panel added the 'Interactions' track in Chrome 116. That's where INP debugging lives. Record an interaction, find it in the track, expand it, and you'll see the three phases: input delay, processing time, and presentation delay. The phase that's red is where you need to dig.

One more thing — Empire UI's components are built with composited animations in mind, so you won't inherit CLS or INP issues from the UI layer. That's not always true of other libraries, especially ones that animate layout properties or use requestAnimationFrame loops without yielding.

Putting It All Together: A Practical Checklist

Don't treat this as a one-time audit. CWV scores drift as you add features, third-party scripts, and new pages. The teams that stay green run automated Lighthouse checks on every PR and monitor field data weekly.

Here's the actual checklist: add fetchpriority="high" to your hero image. Set explicit width and height (or aspect-ratio) on every image and embed. Remove loading="lazy" from above-the-fold images. Add the web-vitals library and pipe the data somewhere you'll actually look at it. Audit your third-party scripts — tag managers and analytics are the biggest INP killers. Use scheduler.yield() or setTimeout in heavy event handlers. Check your fonts — preload the main body font and use font-display: optional if layout shift from FOUT is your CLS culprit.

// next.config.ts — preconnect to your font CDN
const nextConfig = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          {
            key: 'Link',
            value: '<https://fonts.googleapis.com>; rel=preconnect',
          },
        ],
      },
    ];
  },
};

export default nextConfig;

In practice, getting from 'poor' to 'good' on all three metrics takes about a week of focused work for a typical Next.js app. LCP usually clears first. CLS is second. INP is last and hardest because it requires profiling real user interactions, not just load-time metrics.

If you're working on a UI-heavy project and want components that don't fight you on performance, browse the Empire UI library. Everything ships with composited transitions and proper dimension handling baked in — one less layer of CWV debt to carry.

FAQ

What's the difference between Lighthouse scores and CrUX field data?

Lighthouse is a lab test — it runs on a simulated device in controlled conditions. CrUX is real user data from Chrome browsers in the field. Google's ranking algorithm uses CrUX, not Lighthouse. A green Lighthouse score doesn't guarantee green CrUX numbers.

INP replaced FID — do I need to change my monitoring setup?

Yes. Upgrade to web-vitals v3+ and swap onFID for onINP. The function signature is identical, but INP captures a fundamentally different signal — full interaction latency across the page lifetime, not just the first input delay.

My CLS score is good in Lighthouse but poor in Search Console. Why?

Dynamically injected content — ads, cookie banners, chat widgets — often only loads in real browser sessions, not during Lighthouse's controlled run. These show up in field data because real users trigger them. Audit your page with a slow-connection throttle and the Rendering panel's Layout Shift Regions enabled.

How long does it take for CWV fixes to show up in Search Console?

CrUX data is aggregated over a 28-day rolling window. You'll typically see improvements start appearing after 2–3 weeks, with the full impact visible at the 28-day mark. Don't make changes and check Search Console the next morning — it won't reflect yet.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

Core Web Vitals in 2026: LCP, INP, CLS with Real Next.js FixesPerformanceObserver API: Measuring LCP, INP, CLS in CodeWeb Font Loading in 2026: next/font, variable fonts and CLSNext.js Image Optimisation: next/image Deep Dive — Every Prop Explained