Your Lighthouse score is one page load, on one machine, under one set of fake conditions. Here's what your actual visitors experience, how the browser reports it, and how to watch it without adding a second SDK.
A Lighthouse run in your office tells you how your page behaves on one simulated connection, once. It doesn't tell you what a visitor on a mid-range Android phone, on 4G, three time zones away, actually saw. That gap is why field ("real user") monitoring exists, and why it matters for both UX and search.
PerformanceObserver reports paint, largest-contentful-paint and layout-shift entries; Navigation Timing reports TTFB. No library required to see it yourself.PerformanceObserver support for LCP and layout-shift entries has historically lagged Chromium's, so field data collected this way can skew Chromium — check current browser support before assuming it covers your Safari visitors.web-vitals library or CrUX. Start free or see the live demo.If you read nothing else: stop trusting a single lab score, install something that watches real visitors continuously, sort by your worst page, and fix that one first.
Lighthouse and PageSpeed Insights' lab tab both do the same thing: they load your page once, from one location, under simulated network throttling and a simulated mid-tier device profile, and report what happened. That's genuinely useful for debugging — it's reproducible, and you can compare before-and-after on a specific change. But it's a synthetic snapshot, not a description of your traffic.
Field data is the opposite trade-off. It's every real page load, from every real visitor, on whatever device and connection they actually have — a flagship phone on office Wi-Fi, a three-year-old Android handset on patchy 4G, a laptop tethered to a phone hotspot. None of that variance shows up in a single lab run, and for most sites it's most of the traffic. The visitor your Lighthouse score doesn't represent is very often your median visitor.
This isn't just a UX nicety — it's how Google evaluates pages for ranking purposes. Per Google's published guidance, the page experience signal is built from field data, aggregated over real visits, and assessed at roughly the 75th percentile rather than a single score. We're deliberately not quoting exact weighting or a specific data-collection window here, because that's the kind of number that changes without much notice — the qualitative point holds regardless: a fast lab score with slow field data is not a fast page, by Google's own framing.
Practically, that means the right question isn't "what did Lighthouse say," it's "what are the last thousand real visits telling me." Lab tools remain useful for isolating a specific fix. Field monitoring is what tells you whether the fix mattered to anyone.
FCP (First Contentful Paint). The moment the browser paints the first bit of DOM content — text, an image, a canvas element. It answers "has anything shown up yet," which matters more for perceived speed than actual usability.
LCP (Largest Contentful Paint). The moment the largest visible element — usually a hero image, a heading block, or a big text node — finishes rendering. This is the metric most people mean when they say "how fast does the page feel," because it tracks when the main content a visitor came for is actually visible.
CLS (Cumulative Layout Shift). A unitless score for how much visible content jumps around as the page loads — an ad slot that pops in late, a web font swap that reflows text, an image with no reserved space. High CLS is the "I was about to tap that button and the page moved" experience.
TTFB (Time to First Byte). How long the browser waits between requesting the page and receiving the first byte of the response — server processing, database queries, redirects, and network latency all live here. It's the floor everything else is built on: nothing else can start until this finishes.
INP (Interaction to Next Paint). How long the page takes to visually respond after a visitor clicks, taps, or types — across the whole session, not just the first interaction. It replaced First Input Delay as the responsiveness metric because it captures sluggishness anywhere in the visit, not just the very first click.
| Metric | What it measures | Google's "good" threshold |
|---|---|---|
| FCP | Time to first visible paint | ≤ 1.8 s |
| LCP | Time to largest visible element | ≤ 2.5 s |
| CLS | Cumulative visual instability | ≤ 0.1 |
| TTFB | Time to first response byte | ≤ 800 ms |
| INP | Responsiveness to interaction | ≤ 200 ms |
For the broader picture of what to track beyond vitals — visitors, sources, conversions — our guide to measuring website traffic covers the rest of the dashboard these metrics live next to.
None of this requires a vendor. Every metric above (bar INP's full session tracking, which needs a bit more bookkeeping) is exposed directly by the browser through two APIs: PerformanceObserver, which streams performance entries as they happen, and the Navigation Timing API, which gives you a structured timeline of the page's own request/response cycle.
Here's roughly what GhostMetrics — and most RUM tools — do under the hood, stripped down to the essentials so you could wire it up yourself:
// First Contentful Paint
new PerformanceObserver((list) => {
for (const entry of list.getEntriesByName('first-contentful-paint')) {
console.log('FCP', entry.startTime);
}
}).observe({ type: 'paint', buffered: true });
// Largest Contentful Paint (keeps firing until interaction; take the last one)
new PerformanceObserver((list) => {
const entries = list.getEntries();
const last = entries[entries.length - 1];
console.log('LCP', last.startTime);
}).observe({ type: 'largest-contentful-paint', buffered: true });
// Cumulative Layout Shift (sum, ignoring shifts caused by real input)
let cls = 0;
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) cls += entry.value;
}
console.log('CLS', cls);
}).observe({ type: 'layout-shift', buffered: true });
// Time to First Byte, via Navigation Timing
const [nav] = performance.getEntriesByType('navigation');
if (nav) {
// Google's TTFB is measured from the time origin, so it includes
// redirects, DNS and connection setup - not just server think time.
console.log('TTFB', nav.responseStart);
console.log('server time only', nav.responseStart - nav.requestStart);
}
The buffered: true option matters — it backfills entries that fired before your script attached its observer, which is common for FCP and early layout shifts on a fast-loading page. From here, a real implementation batches these values and sends them to a collection endpoint on visibilitychange or page unload, ideally with navigator.sendBeacon so the request survives the tab closing.
The honest caveat: Safari's PerformanceObserver support for the largest-contentful-paint and layout-shift entry types has historically lagged Chromium's — check current browser support before assuming your field data covers Safari visitors. Any field data collected this way is necessarily thinner — or entirely absent — for LCP and CLS on Safari visits, which means aggregate field data for those two metrics skews toward Chrome, Edge, and other Chromium engines. FCP and TTFB are broadly supported across engines, including Safari, so those two numbers are more representative of your full audience.
GhostMetrics collects FCP, LCP, CLS and TTFB from real visitors, using the same PerformanceObserver and Navigation Timing mechanics above (TTFB is the Google definition: response start measured from navigation start, so redirects, DNS and connection setup are included, not just server time), as part of the same ~3 KB analytics script already on your page — there's no second SDK to install and no separate vendor to configure. The vitals go out as one small extra beacon from the tag you already have, not a second library on the critical path. The dashboard shows a Core Web Vitals panel with the aggregate trend, plus a per-page table so you can see which specific URL is dragging the average down, not just a site-wide number that hides the one bad page.
What it doesn't do: GhostMetrics does not capture INP. Session-wide interaction responsiveness needs more plumbing than a lightweight analytics tag is built for. If INP is the number you need — and for interaction-heavy apps, it often should be — use Chrome's open-source web-vitals JavaScript library wired to your own collection endpoint, or reference the aggregate INP figures already published through CrUX and PageSpeed Insights. We'd rather tell you what we don't measure than have you find out from a missing chart.
On privacy: vitals are timings attached to a page load — a millisecond number, not a person. GhostMetrics collects them cookielessly, with no persistent visitor ID and no IP address stored, the same model it uses for pageviews. There's typically nothing here that needs a consent banner, for the same reasons we cover in cookieless tracking explained — though as always, confirm your own jurisdiction's requirements rather than taking this as legal advice.
There's more than one reasonable way to get field data. The right pick depends on whether you need INP today, whether you're willing to run your own collection endpoint, and how much setup you want to own.
| Approach | Real users | Continuous | Extra script | Per-page breakdown | Cost model |
|---|---|---|---|---|---|
| GhostMetrics | Yes | Yes | No — bundled in the analytics tag | Yes | Free plan available; flat paid tier |
Google web-vitals lib + your endpoint | Yes | Yes, as long as you keep collecting | Yes — separate library, plus code you write | Only if you build it | Library is free; you pay for the storage/endpoint you host |
| CrUX & PageSpeed Insights | Yes (aggregated Chrome data) | Yes, rolling | No — no script, it's a public dataset | Limited — often origin-level unless a URL has enough traffic | Free, no signup |
| Dedicated RUM/APM products | Yes | Yes | Yes — separate SDK or agent | Yes, typically detailed | Commercial; check current vendor pricing |
| Lighthouse (lab) | No — synthetic only | Only if scheduled yourself | N/A — a testing tool, not embedded | One URL at a time, one simulated condition | Free |
If you already need broader analytics — visitors, sources, pages — and want vitals as a bonus rather than a second integration, GhostMetrics or a similar bundled tool removes a line of setup. If INP is non-negotiable right now, Google's own web-vitals library is the honest free path, at the cost of building and maintaining your own endpoint. If you just want a directional read with zero setup, CrUX and PageSpeed Insights are free and require nothing on your page — though per-URL data there depends on your site collecting enough traffic to be reported at all. Dedicated RUM/APM platforms go deeper still, generally at commercial pricing worth checking directly rather than assuming. We cover the broader privacy-first tooling landscape, vitals aside, in our roundup of privacy-friendly analytics tools.
<script> tag, or your own web-vitals wiring — to every page's <head>, once, in your header template.width/height (or CSS aspect-ratio), causing late layout shift and a delayed largest paint; a web font that blocks text rendering instead of using font-display: swap or a <link rel="preload">; or render-blocking CSS/JS sitting in the <head> ahead of the content that should paint first.That loop — install, wait, sort, fix one thing, confirm in the field — is the entire discipline. It's slower than chasing a lab score, and it's the only version that tells you the truth.
GhostMetrics collects FCP, LCP, CLS and TTFB from real visitors as part of the same ~3 KB analytics script already on your page — no second SDK, no separate RUM vendor to configure. The dashboard shows a Core Web Vitals panel and a per-page breakdown, so you can find your worst LCP page instead of guessing from a site-wide average.
Being straight about the gap: GhostMetrics does not capture INP. If session-wide interaction responsiveness is the number you need today, pair it with Chrome's web-vitals library or CrUX rather than assuming it's covered. Everything else — the free plan, the single script, the cookieless collection — works the same as GhostMetrics' regular analytics.
Disclosure: GhostMetrics is built by Null Agency and we use it on this site, so we're not a neutral party about our own product — we've kept the claims to what it actually measures, said plainly where it doesn't (INP), and pointed to Chrome's own web-vitals library and CrUX where they fit better. Nothing here is legal advice; confirm your own privacy and cookie obligations for your jurisdiction.