Generate Report →

Achieving 100/100 Mobile Core Web Vitals: LCP, INP & CLS

Mobile LCP under 2.5s, INP under 200ms, CLS under 0.1 — at the 75th percentile of real users. The specific fixes that move each metric, in order of leverage.

A perfect 100 in Lighthouse on your laptop means very little. Google grades your site on the 75th percentile of real Chrome users, which for most businesses means a mid-range Android phone on a congested network, three years into its life, with a battery-saver-throttled CPU.

That is the device you are actually optimizing for. The thresholds you have to clear on it are Largest Contentful Paint under 2.5 seconds, Interaction to Next Paint under 200 milliseconds, and Cumulative Layout Shift under 0.1 — all three, simultaneously, for 75% of visits.

This guide works through each metric in order of leverage: what physically causes it to fail on mobile, the fix that moves it most, and how to confirm the fix landed in field data rather than in a lab simulation.

LCP: the browser cannot paint what it has not found

Largest Contentful Paint measures when the biggest above-the-fold element finishes rendering. On mobile that element is almost always a hero image or a large heading block.

Failures cluster into two causes, and the first is far more common than the second.

Late discovery. The browser’s preload scanner reads raw HTML before executing anything and starts fetching images it sees immediately. Anything that hides the image from that scan delays it by hundreds of milliseconds:

  • background-image in CSS — not discoverable until CSSOM is built
  • Images injected by JavaScript — waits for script download, parse, and execution
  • loading="lazy" on the hero — the single most common self-inflicted LCP wound, usually applied globally by a plugin or a well-meaning build step

The fix is unglamorous, and on a throttled connection it is usually the largest single LCP improvement available to you:

<link rel="preload" as="image"
      href="/img/hero-960.avif"
      imagesrcset="/img/hero-640.avif 640w, /img/hero-960.avif 960w, /img/hero-1440.avif 1440w"
      imagesizes="100vw" fetchpriority="high">

<img src="/img/hero-960.avif"
     srcset="/img/hero-640.avif 640w, /img/hero-960.avif 960w, /img/hero-1440.avif 1440w"
     sizes="100vw" width="1440" height="810"
     fetchpriority="high" decoding="async" alt="...">

Note what is absent: no loading="lazy". Lazy loading belongs below the fold and nowhere else.

Render-blocking head resources. Every synchronous stylesheet and every <script> without defer in <head> pushes first paint back. Inline only the critical CSS your above-the-fold content needs, keeping it small enough to fit comfortably inside the first response, and load the rest asynchronously. Add defer to every script that is not measuring the page load itself.

Web fonts deserve their own note, because they cause a distinct LCP failure when the LCP element is text. Without font-display: swap the browser blocks text rendering for up to three seconds waiting for the font file. Self-host the font, subset it to the characters you use, preload the single weight that appears above the fold, and set font-display: swap.

INP: the metric that exposes your JavaScript

Interaction to Next Paint measures the full round trip from a tap to the next painted frame, and it reports approximately the worst interaction of the visit rather than the first. That change from FID is why so many sites that comfortably passed now fail.

The mechanism is the main thread. JavaScript is single-threaded; while a task runs, nothing else can — not your event handler, not the paint. A 400ms analytics initialization triggered by first scroll will sit directly on top of whatever the user taps next.

Three interventions, in order of effect:

Break up long tasks. Anything over 50ms is a long task. Split heavy work and yield to the browser between chunks:

async function yieldToMain() {
  if ('scheduler' in window && 'yield' in scheduler) return scheduler.yield();
  return new Promise(r => setTimeout(r, 0));
}

async function processAll(items) {
  for (const item of items) {
    handle(item);
    if (performance.now() % 50 < 1) await yieldToMain();
  }
}

Decouple visual feedback from the work. If a tap triggers a filter, render the pressed state and a skeleton first, then do the computation after a requestAnimationFrame. INP stops counting at the next paint — so paint something immediately.

Audit third-party scripts. Chat widgets, tag managers, heatmap recorders, and consent banners are consistently the largest INP contributors on business sites. In Chrome DevTools, run the Performance panel with 4x CPU throttling, interact with the page, and read the long-task attribution. Every entry pointing at a domain you do not own is a candidate for defer, lazy initialization on first meaningful interaction, or removal.

CLS: reserve the space before you need it

Layout shift is the easiest of the three to fix and the most embarrassing to fail, because every cause is known and every fix is mechanical.

CauseSymptom on mobileFix
Images without dimensionsText jumps as each image loadswidth + height attributes, or aspect-ratio in CSS
Web font swapText reflows when custom font arrivessize-adjust / ascent-override on the fallback @font-face
Injected banners, cookie barsWhole page pushed down after loadFixed overlay, or reserve height with min-height
Ads and embedsContent shifts when the slot fillsContainer with explicit aspect-ratio
Late-loading CSSUnstyled content restylesInline critical CSS

Modern browsers compute an implicit aspect-ratio from width and height attributes even when CSS sets width: 100%, so adding the raw pixel dimensions back to every <img> costs nothing and prevents the most common shift outright.

The font-swap shift is subtler. Declaring a metric-adjusted fallback makes the swap nearly invisible:

@font-face {
  font-family: 'Inter Fallback';
  src: local('Arial');
  size-adjust: 107%;
  ascent-override: 90%;
  descent-override: 22%;
}
body { font-family: 'Inter', 'Inter Fallback', sans-serif; }

Also watch CLS after load. The metric accumulates across the entire page lifetime, so an infinite-scroll append or a lazily-inserted related-posts block that pushes the footer counts against you long after first paint.

Field data is the only score that matters

The gap between a Lighthouse 100 and a failing Search Console report is not a bug — the two measure different things. Lighthouse simulates one page load on a modeled device. Chrome User Experience Report data aggregates real sessions across real devices over a rolling 28-day window and reports the 75th percentile.

Practical consequences:

  • A fix deployed today shows partial CrUX movement in about a week and full movement at roughly 28 days. Do not judge a fix on day two.
  • PageSpeed Insights shows both lab and field for the same URL. Read the field panel first; use the lab panel to reproduce.
  • Install a real-user monitoring snippet using Google’s web-vitals library and send LCP, INP, and CLS to GA4 as events. That gives you per-page, per-device-class data within hours instead of weeks, and it is how you find the one template that is dragging the whole origin’s percentile down. If your GA4 events fire before consent resolves you will lose most of these samples — see fixing GA4 event timing and Consent Mode v2.

Segment by device before drawing conclusions. Sites routinely pass overall while failing badly on the mid-range Android segment — and on many business sites that segment is a large enough share of traffic to matter commercially even though the aggregate hides it. Google grades the aggregate; your revenue does not.

The architectural shortcut

There is a category of site that passes all three metrics on the first attempt without any of the tuning above: a static site generator delivering pre-rendered HTML from a CDN edge. No origin round trip, no server-side render time, no framework hydration cost on the main thread.

That is not an argument that a dynamic site cannot pass — plenty do. It is an argument that most of the difficulty in this discipline comes from fighting an architecture that generates HTML at request time and then ships a JavaScript runtime to re-create it in the browser. Teams that migrate typically find LCP and INP resolve as a side effect, leaving only image dimensions and third-party scripts to clean up. The mechanics of that shift are covered in migrating heavy WordPress sites to Hugo, and the build-time image work that feeds a fast LCP is in build-time image optimization with WebP and AVIF pipelines.

Where to start tomorrow

Run PageSpeed Insights on your top five landing pages, mobile tab, and read only the field-data section. Whichever of the three metrics is red on the most pages is your project.

If it is LCP, identify the LCP element in the Lighthouse “Largest Contentful Paint element” audit and confirm it is a plain <img> with fetchpriority="high" and no lazy attribute. If it is INP, open DevTools Performance with 4x CPU throttle and find the long tasks. If it is CLS, add width and height to every image on the template.

One metric, five pages, one week. That sequence resolves more real-world Core Web Vitals failures than any audit tool’s full recommendation list — and if you want the per-template field breakdown done for you, it is part of the Standard MarketLens audit.

Run this article on your site

Audit my site's mobile Core Web Vitals using field data, not lab scores. Identify the LCP element on my top five landing pages and verify it is a plain img tag with fetchpriority='high', no loading='lazy', and a matching preload hint in the head. Then find every image, iframe, and ad slot missing explicit width and height attributes and add them, and list every third-party script blocking the main thread for more than 50ms so I can defer or remove it.

Paste into Claude Code, ChatGPT, Cursor or Gemini. It executes the steps above against your own site.

Frequently Asked Questions

Why does my Lighthouse score say 100 while Search Console reports failing Core Web Vitals?

Lighthouse is a lab test on a simulated device; Search Console reports CrUX field data from real Chrome users at the 75th percentile. A fast laptop on fibre will always score better than a four-year-old Android on congested mobile data. Always treat the field data as the truth and use Lighthouse only to reproduce and debug what the field is telling you.

What replaced First Input Delay, and why is INP so much harder to pass?

Interaction to Next Paint replaced FID in March 2024. FID only measured the delay before the browser began processing the first interaction, so a site with slow handlers could pass trivially. INP measures the full duration from interaction to the next rendered frame, across all interactions in a visit, and reports roughly the worst one — which exposes every long task your JavaScript was hiding.

What is the single most common cause of a failing mobile LCP?

A hero image that the browser discovers late. If the LCP image loads via CSS background, JavaScript, or lazy loading, discovery is delayed until after CSSOM or script execution. Making it a plain img tag with fetchpriority=high, no lazy attribute, and a preload hint often cuts a full second with no other change.

How long after a fix do Core Web Vitals in Search Console update?

The CrUX report is a rolling 28-day window, so a fix deployed today only fully reflects about four weeks later, and Search Console adds its own lag on top. Use the PageSpeed Insights field data and your own real-user monitoring to confirm improvement within days rather than waiting for the Search Console status to flip.

Does passing Core Web Vitals actually improve rankings?

It is a real but small direct ranking signal, and it functions mostly as a tiebreaker between comparable results. The larger effect is behavioural: faster pages get fewer abandonments before paint, which improves engagement metrics, and AI crawlers with short fetch timeouts index fast origins more completely than slow ones.

Continue the track — Static Architecture & Performance