Skip to content
Get your free SEO report→
Back to guides
PerformanceAdvanced10 min Updated Oct 7, 2026

Core Web Vitals: The Page Experience Factor Moves Rankings

LCP, INP, and CLS explained— what each metric measures, what it ignores, the thresholds Google rewards, and the step-by-step fixes that move real-world numbers.

Piyush Rathod avatarWritten by Piyush Rathod
Share:
On this page

A page that loads in 4 seconds, shifts layout when fonts load, and freezes when a user clicks a button will never rank as well as a faster, more stable competitor — all else equal. That is the entire argument for Core Web Vitals in one sentence. Google confirmed them as ranking factors in 2021, updated the INP metric in March 2024, and has steadily increased their weight in the helpful content evaluation. This guide goes deeper than the surface-level advice you have already read: how to read your real data, which fixes actually move the needle, and why most sites are solving the wrong problem.

Key takeaways

  • Google uses field data (real users, real devices) not Lighthouse lab data for ranking.
  • LCP, INP, and CLS each have distinct root causes — fixing one does not fix the others.
  • LCP is almost always the hero image or the font file blocking first paint.
  • INP problems trace back to main-thread congestion from oversized JavaScript bundles.
  • CLS comes from three predictable sources: images without dimensions, late-loading fonts, and dynamic content injected above existing content.
  • Getting to the “good” threshold is the goal — diminishing returns kick in fast past that point.
  • Mobile-first indexing means Mobile SEO is a prerequisite for good CWV scores.

The three metrics and what they actually measure

Google evaluates pages on three distinct axes of user experience, each backed by the Chrome User Experience Report (CrUX):

LCP — Largest Contentful Paint measures when the biggest visible element in the viewport finishes rendering. That element is almost always a hero image, a large heading painted by a web font, or a background-image loaded via CSS. The threshold is under 2.5 seconds for the 75th percentile of real users. LCP between 2.5s and 4.0s gets a “needs improvement” rating. Anything over 4.0s is “poor”. The most common LCP element on content sites is the hero image at the top of the page — typically the product photo, author headshot, or featured blog image.

INP — Interaction to Next Paint replaced FID (First Input Delay) in March 2024 because it captures the full picture of responsiveness, not just the first interaction. INP is the worst interaction latency across an entire page session, measured from when the user clicks or types to when the next frame paints. The target is under 200ms. Note that a page with a fast first click but a sluggish modal or filter update will still fail INP. Google measures INP differently than Lighthouse — this is where most developers get false confidence from lab tools.

CLS — Cumulative Layout Shift is the sum of all unexpected layout shifts that occur during the page lifecycle. A shift is “unexpected” if it occurs without a user action (click, tap, keypress). The threshold is a score under 0.1. Every element that moves after initial render — images loading in, fonts swapping, banners sliding in — contributes to the score proportionally to the distance moved and the viewport size affected.

The good / needs improvement / poor table

Metric Good Needs Improvement Poor
LCP < 2.5s 2.5s – 4.0s > 4.0s
INP < 200ms 200ms – 500ms > 500ms
CLS < 0.1 0.1 – 0.25 > 0.25

Field data vs lab data — why the distinction matters

Google’s ranking systems use CrUX field data: aggregated performance metrics from real Chrome users across real devices and real network conditions. This includes Slow 4G on a Moto G4 in a basement, not the simulated desktop on a fiber connection Lighthouse uses.

Lighthouse lab data is invaluable for debugging — it gives you a reproducible environment, a waterfall chart, and specific resource-level advice. But it will never tell you what your 75th-percentile mobile user on a train experiences. The two data sources tell different stories, and the field data is the one that moves rankings.

Three sources for field data:

  1. Chrome User Experience Report (CrUX) — public dataset on BigQuery and PageSpeed Insights. Covers the top ~1 million domains by traffic.
  2. Search Console → Experience → Core Web Vitals — your own site’s CrUX slice. Grouped by URL pattern so you can find affected groups fast.
  3. web-vitals.js library — first-party collection from your actual users, sending metrics to your own analytics. The only way to see CrUX data for URLs with insufficient traffic to appear in the public dataset.

If Search Console shows “insufficient data” for a URL but you have real traffic, deploy web-vitals.js to your property and collect your own percentile data.

LCP: diagnosing and fixing the largest contentful paint

The LCP element is the largest element in the viewport at the time of the largest paint. For most sites, that is one of three things:

  • A hero image — the featured image, product photo, or background image above the fold.
  • A heading — large text rendered by a web font where the fallback font causes a large paint of the fallback and the swap causes a re-paint.
  • A background image — loaded via background-image in CSS, which Lighthouse does not always catch correctly.

How to find your LCP element

  1. Open Chrome DevTools → Performance panel.
  2. Reload the page with “LCP” highlighting enabled in the rendering settings.
  3. The highlighted element when the largest paint fires is your LCP element.
  4. Cross-reference with WebPageTest’s Filmstrip view — the last frame before LCP shows exactly what the user saw.

The LCP fix hierarchy

1. Preload the LCP image. If the LCP element is an image, preload it explicitly. The browser’s speculative preloader does not always find hero images fast enough, especially if they are set via CSS background-image or loaded lazily with JavaScript.

<link rel="preload" as="image" href="/images/hero.webp" fetchpriority="high" />

Adding fetchpriority="high" tells the browser this is the most important resource on the page. Never preload more than one image — it dilutes the benefit.

2. Use a proper image format and size. WebP or AVIF at the correct display size. A 2400px JPEG at 900kb served as a 600px image is a direct LCP regression. Run srcset with multiple breakpoints so the browser picks the right size for the device. For the LCP image specifically, serve it at its natural display size with fetchpriority="high" and no loading="lazy".

3. Eliminate render-blocking resources above the LCP element. A render-blocking stylesheet or synchronous JavaScript before the LCP element in the HTML blocks the browser’s parser. Every render-blocking script before the hero image is pure delay. Move scripts to defer or async, inline critical CSS, and defer the rest.

4. Fix server response time (TTFB). If the HTML itself takes 1.5s to arrive, no amount of frontend optimization fixes LCP. A TTFB over 600ms is a hosting or CDN problem. Check whether your server is in the same region as your users, whether edge caching is configured, and whether your CDN is serving stale content. The Technical SEO guide covers the full rendering stack audit.

5. Check the web font case. If your LCP element is text, the font file itself may be the bottleneck. Preload the font file directly:

<link rel="preload" as="font" type="font/woff2" href="/fonts/headline.woff2" crossorigin />

Use font-display: optional for above-the-fold text when performance matters more than the flash of unstyled text. Use font-display: swap if perceived performance (no blank space) matters more. Never use font-display: block — it holds the render hostage while the font downloads.

INP: diagnosing and fixing interaction to next paint

INP is the 98th percentile interaction latency across your entire page session. It is not the first click. It is the worst interaction. A page can pass FID (which only measured the first interaction) with flying colors and still score poorly on INP if a filter dropdown, search autocomplete, or “Add to cart” button is sluggish.

How to capture INP data

  • Chrome DevTools → Performance panel — record a session, look for long tasks (red sections on the main thread) around interactions.
  • web-vitals.js with attribution build — onINP gives you the element, event type, and the interaction duration broken down by phase (input delay, processing time, presentation delay).
  • Web Vitals Chrome extension — the fastest way to see INP in real time on any page.

The INP fix hierarchy

1. Code-split your JavaScript. A 1MB main bundle is the leading cause of high INP. The browser cannot parse and execute a massive bundle between a user click and the next frame paint. Split the bundle at route boundaries using your framework’s dynamic import (import()) and load the remaining code on demand. A 200kb initial bundle loads in ~150ms on a median mobile device; a 1MB bundle takes 750ms+.

2. Break up long tasks. Tasks over 50ms on the main thread are where INP dies. The browser’s scheduler API lets you yield between chunks of work:

// Instead of one 400ms task:
async function processLargeList(items) {
  for (const item of items) {
    await scheduler.yield(); // yield to browser between items
    processItem(item);
  }
}

For workloads that cannot be yielded (pure computation), use scheduler.postTask with background: true to move them off the main thread entirely.

3. Defer non-critical JavaScript. Third-party scripts — chat widgets, analytics, tag managers, social embeds — are often loaded synchronously and execute on the main thread. Defer everything that is not essential to the initial interaction. Use Partytown or a similar service worker proxy to run analytics off the main thread without changing their API.

4. Audit event handlers. Open DevTools → Performance → Timings, trigger interactions, and look for bars over 50ms labeled “Long running JavaScript”. Click into the bottom-up tab and sort by “Self Time”. Common offenders: synchronous DOM reads inside click handlers (getBoundingClientRect, offsetHeight), heavy JSON parsing, and synchronous localStorage access.

5. Virtualize long lists. A product listing, table, or feed rendering 1,000+ DOM nodes on a single interaction will block the main thread for that frame. Use windowing (only render what is visible) with a library like TanStack Virtual or built-in browser virtual scrolling. INP improves dramatically when the interaction handler only touches the visible subset.

CLS: diagnosing and fixing cumulative layout shift

CLS is the only CWV metric where the fix is almost entirely deterministic — if you follow the rules, the score drops to zero or near-zero. The three root causes are images without dimensions, font swaps, and dynamic content injection.

The three CLS root causes

Images without dimensions. When the browser encounters an <img> without width, height, or an aspect-ratio CSS declaration, it allocates zero space until the image downloads. The page content below it then shifts down when the image arrives. Always set at least width and height, or apply aspect-ratio via CSS:

<img src="/hero.webp" width="1200" height="630" alt="..." />

Or in CSS:

.hero-image {
  aspect-ratio: 16 / 9;
  width: 100%;
}

When using srcset for responsive images, the width and height attributes still apply to the img element — the browser calculates the intrinsic aspect ratio from them regardless of which source is loaded. The Image SEO playbook covers format selection, compression, and alt text — the full workflow for images that rank and load fast.

Web font swaps. When font-display: swap is used, the browser paints text with the fallback font first, then swaps to the web font when it arrives. If the fallback is significantly different in height, everything below the text shifts. Fix with size-adjust:

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

@font-face {
  font-family: 'MyFontFallback';
  src: url('/fonts/myfont.woff2') format('woff2');
  size-adjust: 98%;
  ascent-override: 95%;
  descent-override: 5%;
}

Measure the fallback font’s metrics and replicate them with size-adjust, ascent-override, and descent-override. Tools like fontaine and @fontsource automate this. Alternatively, use font-display: optional — the browser uses the fallback and never swaps, eliminating font-driven CLS entirely, but users see the fallback font if the font file has not loaded before first paint.

Dynamic content injected above existing content. Cookie banners, “Subscribe to newsletter” modals, chat widgets, and anything that slides or fades in above existing page content shifts the layout. Never inject content above existing content. Cookie banners and consent dialogs should either be at the bottom of the page or the page should be padded at the bottom to reserve their space.

Reserving space for embeds

Ads, YouTube iframes, and third-party widgets should have a reserved container:

<div style="aspect-ratio: 16 / 9; width: 100%;">
  <iframe src="https://youtube.com/embed/..."></iframe>
</div>

The container size is determined by CSS before the iframe loads. The iframe then fills it without causing any layout shift.

The 75th-percentile rule and why you should aim for the 90th

Google evaluates pages at the 75th percentile of user experience across a 28-day window. That means 25% of your real users can have a terrible experience and your ranking does not change — as long as your 75th percentile is in the “good” range.

This has a practical implication: if your LCP is 2.3s (good), but 26% of your users are at 5.0s+ (poor), you are not penalized — but those 26% are bouncing. A poor CrUX score for a URL usually means you are leaking conversions even if rankings look fine.

The recommended target is the 90th percentile in the “good” range. That gives you buffer — a traffic spike, a CDN hiccup, or a third-party script regression will not immediately flip you from good to needs improvement.

What CWV is not

These are common misconceptions that lead to wasted engineering time:

  • It is not a tiebreaker between equal-quality pages. If your content is weaker than a competitor’s, better CWV will not outrank it. CWV can move rankings only when the content quality gap is small.
  • It does not override relevance. A slow page that perfectly answers a query will usually outrank a fast page that partially answers it.
  • It does not penalize poor performance. Google’s systems reward good CWV — they do not actively demote slow pages. Poor CWV is a missed opportunity, not a penalty.
  • It is not a substitute for content quality. Sites that chase Lighthouse scores while ignoring E-E-A-T signals, content depth, and topical authority are solving the wrong problem.

To audit a page’s full on-page signals alongside its Core Web Vitals, paste the URL into our SEO Analyzer — it surfaces title, heading hierarchy, schema markup, internal links, and Core Web Vitals signals in a single report.

How to measure and track CWV over time

Setting up ongoing measurement is as important as the initial fix. CrUX data in Search Console updates roughly monthly and takes 28 days to settle. Use your own web-vitals.js installation to track weekly trends and catch regressions before the next CrUX cycle.

A practical measurement workflow:

  1. Baseline — record current CrUX 75th-percentile values for LCP, INP, and CLS in Search Console before making any changes.
  2. Deploy fix — implement the highest-impact change (usually LCP image preload).
  3. Check lab data — run Lighthouse on the same URL to confirm the change shows in lab data within minutes.
  4. Wait 28 days — do not check Search Console CrUX data for at least 28 days after a fix. Field data takes a full cycle to update.
  5. Compare baselines — if the 75th percentile moved to “good”, lock that change. If not, move to the next fix in the priority list.

Frequently asked questions

Does INP replace LCP as a ranking factor?

No. INP is a separate metric alongside LCP and CLS. All three are independently evaluated by Google’s ranking systems. A page needs all three at “good” to maximize the page experience signal. INP replaced FID because FID only measured the first interaction — INP captures the full session experience, which is why Google switched.

My page passes Lighthouse but Search Console still shows “poor”. Why?

Lighthouse is lab data. Search Console uses CrUX field data from real users. The most common cause of this mismatch is third-party scripts that load differently in the Lighthouse environment (often blocked or not loaded) compared to real browsers. Your real users are loading chat widgets, analytics, and tag managers that Lighthouse skips. Check your CrUX data by device type — Lighthouse simulates mobile, but your CrUX might show poor desktop and good mobile or vice versa.

How does mobile-first indexing interact with Core Web Vitals?

Google uses the mobile version of a page for both indexing and ranking. If your desktop CWV are excellent but your mobile CWV are poor, Google’s ranking systems see the mobile data. Since CrUX is segmented by device type, a poor mobile experience directly affects your rankings even if desktop users see a fast page. Mobile SEO is a direct dependency — a page that renders differently on mobile (dynamic serving, separate m-dot URL, or JavaScript-rendered content) needs explicit CWV validation on the mobile URL.

Can a CDN alone fix my LCP?

A CDN can fix TTFB-related LCP problems — if your server response time is the bottleneck, geographic CDN distribution reduces the time to first byte. But if your LCP is caused by a large hero image, a missing preload, or render-blocking JavaScript, a CDN does nothing for that element’s load time within the browser. CDN is a necessary infrastructure layer for LCP, not a sufficient fix.

My CLS score is 0 but Search Console still shows “needs improvement”. Why?

CLS accumulates over the full page lifetime including after initial load. Dynamic content injected by third-party scripts, cookie consent banners that appear after load, and lazy-loaded images that shift layout all contribute to CLS even after your JavaScript has finished running. Check the “Initial page load” vs “After load” split in web-vitals.js attribution data. Also verify the URL group — Search Console groups URLs by origin pattern and a single slow URL in a group can pull the whole group down.

Wrap-up

Core Web Vitals is a quality floor, not a ranking strategy. Get all three metrics into the “good” range, verify with field data (not Lighthouse), and stop obsessing over milliseconds past that point. The diminishing returns are steep — moving INP from 180ms to 120ms costs the same engineering effort as moving it from 480ms to 200ms, but only the latter moves rankings.

The higher-ROI work after CWV is locked in is content depth, topical authority, and internal linking — the signals that actually determine which page Google chooses when ten pages all have good Core Web Vitals. Use our SEO Analyzer to audit your full on-page SEO alongside your CWV, and cross-reference with the Technical SEO guide for the rendering infrastructure that underpins everything.

Step-by-step

  1. Pull field data

    Open CrUX (Chrome User Experience Report) for your domain. Look at the 75th percentile over 28 days. That is the only number Google uses.

  2. Diagnose LCP

    LCP is usually hero image, hero text, or a render-blocking resource. Lighthouse and WebPageTest show you exactly which.

  3. Diagnose INP

    INP is the slowest interaction in a session. Use the Web Vitals extension to capture it. Then dig into event handlers with the Performance panel.

  4. Diagnose CLS

    CLS is cumulative layout shift. Web fonts, images without dimensions, late-injected ads. Each shift is a contributor.

  5. Ship the fixes

    Preload hero image, defer non-critical JS, code-split the rest, size containers, use font-display: optional.

  6. Measure again

    Wait 28 days for field data to refresh. Compare. Re-invest.

FAQ

Frequently asked questions

The questions readers ask before they put the playbook to work.

No. INP is a separate metric alongside LCP and CLS. All three are independently evaluated by Google's ranking systems. A page needs all three at "good" to maximize the page experience signal. INP replaced FID because FID only measured the first interaction - INP captures the full session experience, which is why Google switched.
Lighthouse is lab data. Search Console uses CrUX field data from real users. The most common cause of this mismatch is third-party scripts that load differently in the Lighthouse environment (often blocked or not loaded) compared to real browsers. Your real users are loading chat widgets, analytics, and tag managers that Lighthouse skips. Check your CrUX data by device type - Lighthouse simulates mobile, but your CrUX might show poor desktop and good mobile or vice versa.
Google uses the mobile version of a page for both indexing and ranking. If your desktop CWV are excellent but your mobile CWV are poor, Google's ranking systems see the mobile data. Since CrUX is segmented by device type, a poor mobile experience directly affects your rankings even if desktop users see a fast page. Mobile SEO is a direct dependency - a page that renders differently on mobile (dynamic serving, separate m-dot URL, or JavaScript-rendered content) needs explicit CWV validation on the mobile URL.
A CDN can fix TTFB-related LCP problems - if your server response time is the bottleneck, geographic CDN distribution reduces the time to first byte. But if your LCP is caused by a large hero image, a missing preload, or render-blocking JavaScript, a CDN does nothing for that element's load time within the browser. CDN is a necessary infrastructure layer for LCP, not a sufficient fix.
CLS accumulates over the full page lifetime including after initial load. Dynamic content injected by third-party scripts, cookie consent banners that appear after load, and lazy-loaded images that shift layout all contribute to CLS even after your JavaScript has finished running. Check the "Initial page load" vs "After load" split in web-vitals.js attribution data. Also verify the URL group - Search Console groups URLs by origin pattern and a single slow URL in a group can pull the whole group down.