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-imagein 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.
| Cause | Symptom on mobile | Fix |
|---|---|---|
| Images without dimensions | Text jumps as each image loads | width + height attributes, or aspect-ratio in CSS |
| Web font swap | Text reflows when custom font arrives | size-adjust / ascent-override on the fallback @font-face |
| Injected banners, cookie bars | Whole page pushed down after load | Fixed overlay, or reserve height with min-height |
| Ads and embeds | Content shifts when the slot fills | Container with explicit aspect-ratio |
| Late-loading CSS | Unstyled content restyles | Inline 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-vitalslibrary 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.
MarketLens