web_vital

One Core Web Vital sample. Five metrics, one entry each, once per document.

FieldValue
Nameweb_vital
Severity9 (INFO)
Written byBrowser tag.
Turned off bydata-auto-vitals="false"

When it fires

The first time the tab is hidden, which is the first moment the numbers are final and the last moment anything is guaranteed to be sent.

One entry per metric per document, and not repeated on client-side navigations: the largest contentful paint of a single-page app happened once, and re-reporting it on every route would turn one measurement into a pile of copies that drag an average around.

A metric the browser does not implement is simply absent, which is the honest answer. CLS is the exception in the other direction: zero is a real CLS and a good one, so it is reported whenever the browser gave us an observer to measure it with, and left out only when it did not.

Attributes

AttributeTypeWhat it holds
firstrun.metricstringLCP, INP, CLS, FCP or TTFB.
firstrun.valuenumberThe sample, rounded to three decimals: enough for CLS, invisible for the millisecond metrics.
firstrun.unitstringms. Absent for CLS, which is unitless.

Every entry also carries the resource its client sends once per batch: session.id, user.id once you have called user(), and the rest of the list on the overview. Anything you pass yourself lands in the same map, and your key wins on a collision.

What it never carries

No rating. Sending good, needs improvement or poor would mean shipping Google's table inside a 4KB budget and then storing the answer on every single row. The server has the same table and classifies when you read, which is also the only way a change to the thresholds ever reaches samples already collected.

No attribution either: which element was the largest paint, or which interaction was the slowest. That is most of what a vitals library weighs, and this product has nowhere to put it.

And no url. The sample belongs to the document, not to a route, so it cannot be grouped by page on its own. Its session.id is the join back to the visit that produced it.

The thresholds

Google's numbers, applied at read time. Under the good column is good, over the poor column is poor, and in between needs improvement.

MetricWhat it measuresGoodPoor
LCPLargest contentful paint: when the main thing appeared.2500 ms4000 ms
INPInteraction to next paint: the worst interaction of the visit.200 ms500 ms
CLSCumulative layout shift: the worst five-second window of movement.0.10.25
FCPFirst contentful paint: when anything appeared.1800 ms3000 ms
TTFBTime to first byte, from navigation timing.800 ms1800 ms

Questions it answers

A widget is a filter, a group by, an aggregate, a time bucket and a limit. These are those five parts written out, and every one of them is something you can build yourself.

QuestionThe query
LCP at the 75th percentileName is web_vital . firstrun.metric is LCP . 75th percentile of firstrun.value
Every metric at a glanceName is web_vital . group by firstrun.metric . 75th percentile of firstrun.value
Worst layout shift seenName is web_vital . firstrun.metric is CLS . maximum of firstrun.value
Whether a release helpedName is web_vital . firstrun.metric is INP . average of firstrun.value . bucket by day

See also