A Lighthouse 100 doesn't mean your site is fast
Lighthouse is a lab simulation on a throttled mid-range phone. Google ranks on field CrUX p75 for LCP, INP and CLS. Here's why the two disagree, and what to measure instead.
- Performance
- Core Web Vitals
- SEO
- Web Platform
Somebody screenshots a green 100 in Lighthouse and puts it in a deck. Two months later, Search Console says the site's LCP is "Needs improvement" on mobile for a big chunk of its URLs. Both things are true at once, and the reason they're both true is the single most misunderstood thing about web performance measurement.
Lighthouse is a lab test. Google's page-experience signal is a field measurement. They are not the same number, they don't use the same inputs, and one of the three Core Web Vitals cannot be measured in a lab at all. A perfect Lighthouse score tells you your site is fast under one specific simulated condition, once. It tells you very little about whether your actual users are having a fast time.
What Lighthouse actually does
Run Lighthouse and you get a mobile audit under a fixed, deliberately pessimistic set of conditions:
- Device emulation of a mid-range Android phone — Lighthouse 12 emulates a Moto G Power, having used the older Moto G4 for years.
- CPU throttling, typically a 4× slowdown applied to your machine's actual CPU.
- Network throttling to a simulated slow 4G: roughly 1.6 Mbps down, 750 Kbps up, 150ms RTT.
- A single cold run with an empty cache, no service worker warm, no HTTP cache, no browser extensions, no third-party consent banner having already been dismissed.
That last point matters more than people expect. Lighthouse measures the worst realistic first visit. Your real traffic is a mixture of first visits, repeat visits with warm caches, users on fibre with an M-series MacBook, and users on a five-year-old budget Android on congested mobile data. Lighthouse models exactly one point in that distribution and then reports it as a score out of 100.
It's also noisier than the three-significant-figure presentation suggests. Run the same page five times on the same machine and scores commonly move by 3–8 points, more if there's third-party JavaScript involved, more again if you're running it on a laptop that's also running your dev server, Slack and forty Chrome tabs. Simulated throttling estimates timings from an unthrottled trace rather than measuring them directly, which adds its own modelling error. A change from 92 to 96 is usually noise. A change from 92 to 61 is a signal.
The score is a weighted average, and the weights are opinionated
The Lighthouse performance score isn't a measurement — it's a weighted blend of five metrics, each scored against a log-normal curve derived from real-world HTTP Archive data. In Lighthouse 10 and later, the weights are:
| Metric | Weight | What it captures |
|---|---|---|
| Total Blocking Time (TBT) | 30% | Main-thread blocking between FCP and TTI |
| Largest Contentful Paint (LCP) | 25% | When the main content finishes rendering |
| Cumulative Layout Shift (CLS) | 25% | Visual stability during load |
| Speed Index (SI) | 10% | How quickly content is visually populated |
| First Contentful Paint (FCP) | 10% | First pixel of content |
Two things fall out of that table.
First, TBT is the largest single lever, at 30%. If you want to move a Lighthouse score cheaply, you cut and defer JavaScript. That's usually good for users too, so no complaints — but be aware you're optimising a proxy.
Second, and more importantly: Interaction to Next Paint is not in the table at all. INP replaced FID as a Core Web Vital in March 2024. It measures the latency of real interactions — taps, clicks, key presses — from input to the next frame painted. Lighthouse can't measure it, because Lighthouse doesn't interact with your page. Nobody clicks anything. So Lighthouse substitutes TBT as a proxy for interaction responsiveness.
TBT is a reasonable proxy for load-time responsiveness. It is a poor proxy for what INP actually catches: the expensive React re-render when someone opens a filter panel, the 400ms handler on your "add to cart" button, the modal that takes half a second to appear because it hydrates on open. None of that happens during a Lighthouse run. You can score 100 and still ship an INP of 600ms.
What Google actually uses
The page-experience signal in Google Search uses field data from the Chrome User Experience Report (CrUX), not your local Lighthouse run. Specifically:
- Real Chrome users, opted into usage statistics, on public URLs with enough traffic to meet CrUX's reporting threshold.
- The 75th percentile of each metric — so the experience of the slowest quarter of your visits, not the average.
- A rolling 28-day window, which means changes you ship today show up in the data gradually over the following month.
The thresholds are fixed and public:
| 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 |
A URL passes Core Web Vitals when all three are in the "good" band at p75.
The p75 detail is the one that catches teams out. Optimising for your median user does nothing here. If 70% of your traffic is on decent hardware and 30% is on cheap Androids over patchy mobile data, p75 sits squarely inside that second group. Your dashboards showing a healthy median LCP of 1.8s can coexist happily with a failing p75 of 4.3s.
How to read the data you're actually graded on
Three sources, in rough order of how quickly you'll need them.
Search Console → Core Web Vitals report. This is CrUX data grouped into URL patterns, split by mobile and desktop. It's the closest thing to seeing what Google sees. It's also slow — 28-day window, plus reporting lag — so treat it as a monthly scorecard, not a feedback loop.
The CrUX dashboard / API. Origin-level and URL-level histograms, month by month. Useful for the "did that release actually help" question and for comparing yourself against competitors, since CrUX covers any sufficiently trafficked public origin, not just yours.
Your own RUM. This is the one most teams skip and the one that actually lets you fix things. The web-vitals npm package is small, maintained by the Chrome team, and reports the same metric definitions Google uses:
import { onLCP, onINP, onCLS } from 'web-vitals';
function send({ name, value, rating, id, attribution }) {
navigator.sendBeacon('/rum', JSON.stringify({
name, value, rating, id,
path: location.pathname,
element: attribution?.element, // what caused it
conn: navigator.connection?.effectiveType,
}));
}
onLCP(send, { reportAllChanges: false });
onINP(send);
onCLS(send);
Two details worth keeping. Use sendBeacon (or fetch with keepalive) so reports survive the page unloading — INP and CLS finalise late, often at page hide. And pass the attribution build's attribution object through: knowing your LCP is 3.9s is mildly useful, knowing it's the hero image on the category template on slow connections is actionable.
Once you have your own RUM, segment it. By route, by device class, by connection type, by whether the session was a first visit. That segmentation is what turns "our LCP is bad" into "our LCP is bad on the product listing page for users arriving cold from Instagram."
When 100 is worth chasing, and when it's vanity
Chasing the score is worth it when:
- The page is genuinely new and you have no field data yet. Lighthouse is the only signal available, so use it.
- You're regression-testing in CI. A score that drops 20 points on a PR is a good tripwire, even though the absolute number means little.
- The audit list is telling you something specific — render-blocking resources, unsized images, oversized bundles. The recommendations are frequently better than the score.
- You're on a marketing or content site where cold first visits genuinely are most of the traffic. Here the lab conditions are a fair model of reality.
It's vanity when:
- The app is authenticated and interaction-heavy. A logged-in dashboard's problem is INP, and Lighthouse has nothing to say about INP.
- You're grinding 94 to 100. Those last points are curve-fitting against a log-normal scoring function. Users can't feel them; p75 won't move.
- Field data disagrees with you. If CrUX says LCP p75 is 3.6s and Lighthouse says 98, the field data is right and your lab conditions are wrong. Go find out which segment of real traffic you've been ignoring.
- You optimised for the audit rather than the user. Lazy-loading the LCP image improves nothing and often makes LCP worse; it just stops an audit complaining.
The one client site we've shipped so far — Puratan Ayurveda, an Angular storefront on a Laravel back end — is a good example of the shape of this problem rather than a benchmark: a product page's lab score and its real p75 answer different questions, because most of that traffic arrives on mid-range Android over mobile data, and the interactions that matter (variant selection, cart) happen long after Lighthouse has stopped watching.
What we'd do
Our default order of operations, and we'd argue it in any project kickoff:
- Instrument field RUM on day one.
web-vitalswith attribution, beaconed to whatever you already use for analytics. Before any optimisation work. You cannot prioritise what you can't segment. - Set the KPI as CrUX p75 passing all three, per template, on mobile. That's the number that's actually graded.
- Put Lighthouse in CI as a tripwire, with a budget rather than a target — fail the build on a meaningful regression, not on missing 100.
- Use Lighthouse's diagnostics, not its score, when you're debugging a specific field regression you've already found in RUM.
- Measure INP separately and deliberately, because nothing in the lab will surface it. Long tasks in interaction handlers, hydration on interaction, and heavy re-renders are where it lives.
TL;DR
Lighthouse simulates one cold visit on a throttled mid-range phone and blends five metrics into a weighted score — 30% TBT, 25% LCP, 25% CLS, 10% SI, 10% FCP. Google grades you on something else entirely: real Chrome users' 75th-percentile LCP, INP and CLS over a rolling 28 days, from CrUX.
The gap is structural, not a bug. INP, the Core Web Vital that replaced FID in March 2024, cannot be measured without interaction, so Lighthouse approximates it with TBT and misses everything that happens after load.
Instrument field RUM first, make CrUX p75 the KPI, and keep Lighthouse where it earns its keep — as a regression tripwire in CI and a diagnostic tool when you already know what's broken. Never as the number you report.
If your Search Console report and your Lighthouse score are telling you different stories, we'd be happy to dig into it.
