Core Web Vitals Real User Monitoring, Without a Heavy Script

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.

By Null Agency · Updated September 7, 2026 · Written for developers who trust field data over a single lab run

TL;DR — lab scores are a guess, field data is the answer

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.

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.

Lab data vs. field data: why your Lighthouse score lies

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.

The five metrics, defined plainly

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.

MetricWhat it measuresGoogle's "good" threshold
FCPTime to first visible paint≤ 1.8 s
LCPTime to largest visible element≤ 2.5 s
CLSCumulative visual instability≤ 0.1
TTFBTime to first response byte≤ 800 ms
INPResponsiveness 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.

How browser vitals measurement actually works

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.

Where GhostMetrics fits — and where it deliberately doesn't

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.

Comparing your options for real-user vitals

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.

ApproachReal usersContinuousExtra scriptPer-page breakdownCost model
GhostMetricsYesYesNo — bundled in the analytics tagYesFree plan available; flat paid tier
Google web-vitals lib + your endpointYesYes, as long as you keep collectingYes — separate library, plus code you writeOnly if you build itLibrary is free; you pay for the storage/endpoint you host
CrUX & PageSpeed InsightsYes (aggregated Chrome data)Yes, rollingNo — no script, it's a public datasetLimited — often origin-level unless a URL has enough trafficFree, no signup
Dedicated RUM/APM productsYesYesYes — separate SDK or agentYes, typically detailedCommercial; check current vendor pricing
Lighthouse (lab)No — synthetic onlyOnly if scheduled yourselfN/A — a testing tool, not embeddedOne URL at a time, one simulated conditionFree

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.

Numbered how-to: find and fix your worst LCP page

  1. Install the tag. Add your RUM script — GhostMetrics' single <script> tag, or your own web-vitals wiring — to every page's <head>, once, in your header template.
  2. Wait for real traffic. A lab score updates the second you refresh; field data needs actual visits to accumulate before it means anything. Give it enough traffic to cover your normal mix of devices and networks — a slow trickle of visits on one busy afternoon isn't representative yet.
  3. Open the per-page table. Sort by LCP rather than staring at the site-wide average, which can hide one badly-behaving page behind a dozen fine ones.
  4. Find the worst offender. It's very often a landing page with a large hero image, a product page with an unoptimized gallery shot, or any page that loads a custom web font before showing text.
  5. Diagnose the specific cause. The usual suspects: an image with no explicit 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.
  6. Ship the targeted fix. Size the image, preload or swap the font, defer or inline the critical CSS — one change at a time, so you know which one moved the needle.
  7. Confirm the field number moved. Not the lab score — the same per-page table, after enough new traffic has landed on the fixed page. A better Lighthouse run proves the code changed; a better field number proves it mattered to a real visitor.

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.

Real-user vitals, bundled into the analytics tag you already have

FCP · LCP · CLS · TTFBNo INP~3 KB, one scriptPer-page tableCookieless

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.

Start free — no card, no banner See the live demo

FAQ

What's the difference between lab data and field data for Core Web Vitals?
Lab data is a single synthetic run — Lighthouse or PageSpeed Insights loading your page once, under fixed simulated network and device conditions. Field data is real visitors, on their actual devices and networks, measured continuously. Per Google's published guidance, its ranking signal is built from field data at the 75th percentile, not a single lab score.
What are the Core Web Vitals thresholds for LCP, CLS, and INP?
Google's documented "good" thresholds are LCP at or under 2.5 seconds, CLS at or under 0.1, and INP at or under 200 milliseconds. FCP at or under 1.8 seconds and TTFB at or under 800 milliseconds are also published as good thresholds.
Does GhostMetrics track INP?
No. GhostMetrics captures FCP, LCP, CLS and TTFB from real visitors, but it does not capture INP. For INP, use Chrome's open-source web-vitals JavaScript library with your own collection endpoint, or reference CrUX and PageSpeed Insights.
Why does field data skew toward Chrome users?
Safari's PerformanceObserver support for the largest-contentful-paint and layout-shift entry types has historically lagged Chromium's (check current browser support), so any field measurement of LCP and CLS collected this way is thinner or absent for Safari visitors and weighted toward Chrome, Edge, and other Chromium-based browsers.
Do I need a cookie banner for Core Web Vitals tracking?
Vitals are timings, not identifiers — a millisecond value tied to a page load, not a person. Collected cookielessly, with no persistent visitor ID, there's typically nothing that requires consent. This is general guidance, not legal advice; confirm your own jurisdiction's requirements.

Keep reading

All Analytics Guides

The full library, one index

Add Analytics to a Static Site

Snippets for Astro, Hugo, 11ty, Next.js and more

Track Custom Events Without GA

Clicks, signups and downloads — real code

GhostMetrics Live Demo

The real dashboard, live data

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.