What "passes Core Web Vitals" measures, over which users, at which percentile, and how the definition changes underneath you without anyone shipping code.
Contents · 11 sections
A slide says the site "passes Core Web Vitals". A quarter later the Search Console report turns amber, nothing relevant has shipped, and someone wants to know who broke it. That argument can't be settled until somebody can say precisely what was measured, for whom, over what period, and by whose rules.
The quick answer is "three metrics and three thresholds": LCP at most 2.5 seconds, INP at most 200 milliseconds, CLS at most 0.1. It's correct and nearly useless. It leaves out the percentile, the population, the device split, the 28-day window, the fact that one browser vendor's dataset is the referee, and the fact that Chrome revises how it measures each metric in public changelogs that most teams have never opened.
This piece settles the vocabulary the rest of the series depends on: what "passes" means, where the numbers came from, how a metric gets into and out of the set, where definition changes are recorded, and why a pass rate can move with nobody shipping anything. Every status fact below was checked on 18 October 2026.
"Core Web Vitals" names three different things
"Core Web Vitals" can mean any of three things, and a conversation that mixes them up goes in circles.
The first is a program. Google's Web Vitals page describes Web Vitals as an initiative by Google, and Core Web Vitals as the subset that applies to all web pages, should be measured by every site owner, and is surfaced across Google's tools. The program decides which metrics are in the set, what the thresholds are, at which percentile they're judged, and how metrics enter and leave. None of that is in any specification. It lives on web.dev, on a page that, as of today, was last updated on 31 October 2024.
The second is a set of specifications. LCP is defined by the Largest Contentful Paint spec, INP is built on the Event Timing API, and CLS rests on the Layout Instability API. These define the performance entries a browser exposes: which paint counts as contentful, when an event's duration ends, how a single layout shift is scored. They say nothing about 2.5 seconds or the 75th percentile.
They also stop short of the per-page number. The step from a stream of entries to one value per page view is written on web.dev, not in a spec. The INP article defines INP as the longest interaction observed, ignoring one highest interaction for every 50; the article says most page views have fewer than 50 interactions, so it's usually the single worst one. The CLS article defines CLS as the largest session window: a burst of shifts less than 1 second apart, capped at 5 seconds in total. Change either rule and every site's number moves while the specs stay exactly as they were.
The third is a dataset: the Chrome UX Report, CrUX. The program's page says CrUX powers PageSpeed Insights, Chrome DevTools and Search Console's Core Web Vitals report. CrUX has its own rules about which users, pages and origins count, documented in the CrUX methodology, and its own idea of what a page load is. The CrUX and RUM guide says CrUX counts back/forward cache restores as page navigations, measures content inside iframes that page JavaScript can't see, and doesn't report pages loaded in background tabs. When Search Console tells you a group of URLs fails, it's CrUX's aggregation of Chrome's implementation of the specs, judged against the program's thresholds.
Fig. 1 shows how those layers stack. A browser implements what the spec defines, CrUX aggregates what that browser reports, and the program judges the aggregate. Each arrow is a place where a change can happen independently of the others.
Keeping the layers apart matters because they change on different schedules and live in different places. The specs are edited in W3C and WICG repositories. Chrome's implementation can change in any release. CrUX's methodology and its monthly releases are run by the Chrome team. The program's rules sit on a web.dev page. A team that tracks only one of them will be surprised by the others.
The definition fits in one sentence, and every word carries weight
Here is the current definition, assembled from the program's own pages. A page passes Core Web Vitals when, at the 75th percentile of page loads, segmented by mobile and desktop, its LCP is at most 2.5 seconds, its INP is at most 200 milliseconds and its CLS is at most 0.1. The Web Vitals page gives the three thresholds, the 75th percentile and the mobile and desktop segmentation, and says tools should consider a page passing only if it meets the targets for all three metrics.
There's one exception that matters for content-heavy sites. The CrUX and RUM comparison guide adds that if INP is not present for pages with no interactions, only LCP and CLS need to pass. A page that nobody clicks, taps or types on can't fail on responsiveness. It can't pass on it either; the metric is absent.
Each metric also has a "poor" band. The thresholds article lists LCP above 4,000 ms, INP above 500 ms and CLS above 0.25 as poor, with everything between good and poor rated "needs improvement". The INP article spells out the INP bands the same way: 200 ms or less is good, above 200 and up to 500 needs improvement, above 500 is poor.
The classification rule is easy to get backwards. The thresholds article says a site is classified good for a metric if at least 75 percent of page views meet the good threshold, and poor if at least 25 percent of page views fall in the poor band. Those are the same statement as "the p75 value is in the good band" and "the p75 value is in the poor band". It's a statement about the distribution, never about any one visit.
Here is that rule as code. It assumes you already have p75 values for one form factor; it doesn't compute them (PRF-09, on the statistics of RUM, covers why you can't average your way to a p75).
type P75s = { lcp: number; cls: number; inp?: number };
// Good thresholds from web.dev/articles/vitals (as of October 2026).
const GOOD = { lcp: 2500, inp: 200, cls: 0.1 } as const;
// All three must be good at p75. A missing INP (no interactions)
// is not a failure: then only LCP and CLS have to pass.
export function passesCoreWebVitals(p75: P75s): boolean {
const lcpOk = p75.lcp <= GOOD.lcp;
const clsOk = p75.cls <= GOOD.cls;
const inpOk = p75.inp === undefined || p75.inp <= GOOD.inp;
return lcpOk && clsOk && inpOk;
}
passesCoreWebVitals({ lcp: 2400, cls: 0.05, inp: 210 }); // false
passesCoreWebVitals({ lcp: 2400, cls: 0.05 }); // trueRun it twice, once for phone and once for desktop. "Segmented across mobile and desktop" means a site can pass on one and fail on the other, and those are two separate verdicts rather than one averaged verdict. The CrUX History API actually splits traffic three ways, into PHONE, TABLET and DESKTOP, and returns a combined record if you don't ask for a form factor. The CrUX and RUM guide notes that many tools don't expose tablet data, or fold it into mobile or desktop. So "mobile" in the program's sentence and PHONE in the dataset aren't quite the same bucket, and two tools can disagree about a site's "mobile" verdict without either being wrong.
The population is the part people forget. CrUX only includes Chrome users who have usage statistics reporting on, sync their browser history and haven't set a sync passphrase, on desktop Chrome or Chrome on Android. The methodology explicitly excludes Chrome on iOS, Android WebView and other Chromium browsers such as Microsoft Edge, and says Chrome does not publish what proportion of users meet those criteria. The time window is fixed too: CrUX data is a 28-day sliding window that you can't change.
So the honest long form of "we pass Core Web Vitals" is: among opted-in Chrome users on desktop and Android who visited this page or origin over the last 28 days, at least three in four page loads had good LCP, at least three in four had good CLS, and (if anyone interacted) at least three in four had good INP, judged separately per form factor. Note that those are three separate three-in-fours. They don't have to be the same page loads, so a passing site can have more than a quarter of its visits fail at least one metric. That's a much narrower claim than the short form, and it's the only one the data supports.
The thresholds are a compromise between research and what the web already did
The thresholds come from two criteria, and perception research is only one of them. The thresholds article names two criteria: a "good" threshold must correspond to a high-quality user experience, and it must be achievable by existing web content. Achievable has a precise meaning. The article requires that at least 10% of origins in CrUX meet the candidate "good" threshold, and that well-optimized sites meet it consistently. The "poor" threshold is set, by default, so that the worst-performing 10 to 30 percent of origins land in it.
LCP shows how the two criteria interact. The article walks through HCI research by Card, Newell and Miller that puts the point where users lose focus somewhere between roughly 0.3 and 3 seconds, then narrows the candidates to 1 to 3 seconds because First Contentful Paint's "good" threshold was already 1 second and LCP typically comes after FCP. Then CrUX decides. As of April 2020, only 3.5% of phone origins and 6.9% of desktop origins met a 1-second LCP, which fails the 10% rule. At 2.5 seconds it was 42% of phone and 51% of desktop origins. The article says 1.5 and 2 seconds weren't consistently achievable by top-performing sites, while 2.5 was. For "poor", 4 seconds put roughly 26% of phone origins in the poor band, inside the 10-30% target.1
INP is the clearest case of achievability overruling research. The article concludes that the research points to 100 ms as the ideal "good" threshold, citing Michotte's causality experiments and work on touchscreen button latency. But in May 2022 CrUX data, only 12% of phone origins had a p75 INP of 100 ms or less, against 56% at 200 ms. The poor threshold moved for the same reason. Across all origins, 300 ms looked like a reasonable poor line, but among the top 10,000 origins, 55% of phone origins would have been "poor" at 300 ms and 24% at 500 ms. The article says INP correlates inversely with popularity, so it picked 500 ms to keep popular sites inside the 10-30% band.
CLS had no perception literature to draw on at all. Google's internal testing found shifts of 0.15 and higher consistently disruptive and 0.1 or lower noticeable but tolerable. CrUX data from April 2020 showed about half of origins already at 0.05 or lower, but the article chose 0.1 because of third-party embeds whose height isn't known until they load, and says it hopes a stricter threshold becomes possible later.
Then there's the device question. The article argues that user expectations don't depend on device even when achievability does, so it uses one threshold for both phone and desktop. It also concedes that because mobile is more constrained, the thresholds "more likely represent mobile thresholds". The INP achievability table shows how lopsided that is: in May 2022, 56% of phone origins met 200 ms but 96% of desktop origins did. A desktop pass clears a bar that was sized for phones.
Two things follow for anyone setting internal targets. First, the thresholds are deliberately not aspirational: by construction, a meaningful share of the web already met them when they were set. The article itself says many sites would benefit from optimizing beyond "good". Second, the achievability data is dated (April 2020 for LCP and CLS, May 2022 for INP). The article was last updated on 7 May 2025, and I found no statement that the thresholds have been re-derived against newer CrUX data since.
The 75th percentile is chosen to resist outliers, not to describe your typical user
The choice of percentile rests on two goals that pull in opposite directions. The percentile should be high enough that most visits experienced the target, and the value at that percentile shouldn't be decided by a handful of outliers. The article's example is concrete: with 100 visits, it takes only 5 outlier samples to move the 95th percentile, but 25 to move the 75th. Google concluded p75 strikes a reasonable balance, and three of every four visits at or better than target is the guarantee it buys.
Be clear about what that leaves out. A p75 metric is, by design, indifferent to your worst quarter of page loads. If your slowest 20% of visits are users on old Android phones in a market you care about, a passing p75 says nothing about them. That's not a flaw in the metric; it's a choice made to keep classification stable across the whole web. It does mean that "we pass" and "our users are fine" are different claims, and an internal SLO is free to use a higher percentile for a segment you care about, as long as you report it as your own number and not as Core Web Vitals.
The percentile also interacts with the 28-day window in a way that catches teams out after a fix. CrUX's History API publishes one collection period per week, each covering the previous 28 days, so consecutive points share three weeks of data. A fix shipped on a Monday shows up as a gradual slide across four weekly points, not a step. If the slide stops partway, that's not necessarily a partial fix; it might be that the old weeks haven't rolled out of the window yet.
Metrics move from experimental to pending to stable, and stable is not forever
The program has a lifecycle with three phases, and the Web Vitals page defines them. Experimental metrics are prospective Core Web Vitals that may still change significantly. Pending metrics have passed testing and feedback and have a defined timeline to becoming stable; they stay pending for at least six months so the ecosystem can adapt. Stable metrics are the current Core Web Vitals. As of today the page lists LCP, CLS and INP as stable, and nothing else in any phase.
The stable phase comes with a promise and a caveat in the same paragraph. The page says stable metrics won't change more than once per year, and that any change will be communicated in the metric's documentation and changelog. It also says stable metrics aren't necessarily permanent, and gives FID as the example of a stable metric that was retired.
The lifecycle is younger than the metrics it governs. A Wayback Machine snapshot of the page from March 2021 lists LCP, FID and CLS with no lifecycle section at all. INP is the first metric I can find documented going through all three phases, and Fig. 2 traces it. The web.dev post setting the switch date says Chrome introduced INP as an experimental metric in May 2022. Google Search Central's May 2023 announcement says that after another year of testing, the Chrome team decided to promote it to replace FID effective March 2024. The April 2023 CrUX release, published 10 May 2023, says INP was moving from experimental to pending in that release, and started publishing it without the experimental prefix in BigQuery and the APIs. The same March 12 post named the date, and the launch post, last updated 12 March 2024, says INP was now stable, that Chrome was deprecating FID, and that developers had until 9 September 2024 to move off it. That's roughly ten months in pending, comfortably over the six-month minimum.
FID's exit shows that "retired" has several dates depending on which layer you ask. The program removed it from the set on 12 March 2024, and the March 12 post says Search Console dropped it the same day, while PageSpeed Insights and CrUX got a six-month deprecation period. The Chromium changelog index lists FID under deprecated Web Vitals with the date 9 September 2024, and the FID changelog has the same date. CrUX's August 2024 release, published 10 September 2024, removed FID from the CrUX API that day and made the 202408 BigQuery table the last with FID data. The September 2024 release confirms it as the first month without FID. If a dashboard still has a FID panel, those are the dates its data stopped arriving.
The lifecycle also shows up in the data's field names. Pending INP lived alongside an experimental-prefixed field for a while; the June 2023 release warned the prefixed fields would be removed in 30 days, and the July 2023 release confirms they were removed. Today the same naming convention is back. On 13 October 2026 CrUX published experimental_largest_contentful_paint_with_softnavs, along with INP and CLS equivalents, which blend soft navigations in single-page apps with hard navigations. The post says Google aims to graduate them from experimental in May 2027, subject to feedback, and that Search Console will switch to the new data once they graduate. I'll come back to what that does to pass rates.
The changelogs are where the definition actually lives
If the program's page is the constitution, the Chromium metrics changelogs are the case law. The Web Vitals page links to them as the public changelog for all Web Vitals, and each metric has its own: LCP, CLS and INP. Each entry is filed under a Chrome milestone and carries a label, most often "Metric definition improvement", "Metric bug fix", "Launch feature" or "Implementation optimizations". The latest entries as of today are Chrome 153 for LCP, Chrome 150 for INP and Chrome 145 for CLS.
Recent entries give a feel for what counts as a change.
- LCP, Chrome 147. A definition improvement: Chrome now emits LCP candidates to
PerformanceObserverbased on the largest painted image instead of the largest pending image, aligning with the W3C spec. Before, a slow hero image still downloading could suppress entries for text that had already painted. - LCP, Chrome 151. A launch feature: Chrome requests a frame as soon as the first video frame's timing arrives, so LCP entries for video aren't delayed on an idle page.
- CLS, Chrome 145. Two definition improvements:
LayoutShiftattribution rectangles are reported in CSS pixels rather than physical pixels, andsourcesis sorted by impact area, largest first. The scores themselves don't change. - INP, Chrome 144, 147 and 150.
performance.interactionCountlaunched to stable in Chrome 144. Chrome 147 has a definition improvement for early interaction ID assignment and a context-menu fallback. In Chrome 150, nested clicks such as a<label>click forwarding to its form control getinteractionId = 0, which the INP changelog describes as a bug fix to align with web standards.
The most useful part of each detailed entry is the section headed "How does this affect a site's metrics?", because it tells you which layer a change reaches. The Chrome 147 LCP entry is explicit that there is no change to CrUX or UKM field metrics, because those evaluate the largest candidate on interaction or unload. What changes is what your RUM script sees: more intermediate candidates, and a different final candidate when the user interacts before a big image finishes. That's a change you'd see in your own data and never in Search Console.
That entry also changed the plan for this article. I'd intended to chart an origin's weekly CrUX p75 LCP across the Chrome 147 rollout to show a changelog-driven shift. The changelog's own text says CrUX wouldn't move, so that chart could only show noise. The method survives in the recommendations; the evidence section below uses cases where Google itself attributed a CrUX movement to a changelog item.
One more caution about dates. The detailed changelog pages say Chrome 147 reached stable in February 2026 and Chrome 151 in June 2026. The Chromium Dash schedule, which I read from its JSON endpoint, gives stable dates of 7 April 2026 for Chrome 147 and 28 July 2026 for Chrome 151. Chrome 145's page says February 2026, which does match its 10 February stable date. The two mismatched months do line up with something else in Chromium Dash: the feature freeze, 23 February 2026 for Chrome 147 and 15 June 2026 for Chrome 151, and the file names (2026_02, 2026_06) carry the same months. That fits the prose having been written around feature freeze, but it's my guess, not something the pages say. I trust Chromium Dash for release dates, since it's the schedule itself. It also shows that "stable" is a window rather than a day: for Chrome 147 it lists an early-stable date of 25 March and a stable date of 7 April. If you annotate a chart with a changelog item, take the dates from Chromium Dash.
"Once per year" covers less than it sounds like
The program says stable metrics won't change more than once per year, and its "Changes to Web Vitals" section promises updates with "prior notice and a predictable, annual cadence". That second sentence is old: the March 2021 snapshot already has it, word for word. Now set it against the changelogs. CLS has "metric definition improvement" entries under Chrome 88, 89, 90, 91 and 93, whose stable dates per Chromium Dash run from 19 January to 31 August 2021, all while that promise was on the page. Chrome 91 is the one where CLS switched to the maximum session window, which changed the formula itself. LCP has definition improvements under Chrome 112 and 116, stable on 4 April and 15 August 2023.
Recent years look tidier. So far in 2026 each metric has definition improvements in exactly one release: CLS in Chrome 145, and LCP and INP both in Chrome 147. INP's previous one was Chrome 135, stable on 1 April 2025. If the promise is counted per metric per calendar year, 2025 and 2026 keep it, while CLS in 2021 and LCP in 2023 didn't. Nothing on the page says how it's counted.
The wording of the thresholds has moved too. The 2021 snapshot says pages should have a CLS "of less than 0.1"; today's page says 0.1 or less, and the thresholds article uses ≤ for all three. Read literally, a CLS of exactly 0.1 wasn't good under the old sentence and is good under the new one. I couldn't find a changelog entry or announcement for that edit, and I don't know whether any tool ever applied the strict reading.
The page does leave itself room: stable metrics "can be subject to bug fixes and definition changes". My reading is that the once-a-year promise covers the published definition and thresholds, the thing on web.dev, while implementation fixes that bring Chrome closer to that definition ship whenever they're ready. I couldn't find any public document that draws that line, or any log that says which change was the annual one. Until one exists, assume your field numbers can shift with any Chrome release, and treat the yearly promise as covering thresholds and set membership at most.
There's a second wrinkle that makes "which Chrome version?" an imperfect proxy. Some changes roll out by flag, independent of the version number. The Chrome 112 low-entropy image change says it rolled out to existing installs from roughly 6 April 2023, and that versions back to Chrome 109 also picked it up. The Chrome 128 INP change says the experiment first landed in Chrome 125 and was slowly rolled out. Two users on the same version can be measured by different definitions.
A definition change can move the web's pass rate while nobody ships anything
The CrUX BigQuery dataset is updated on the second Tuesday of each month, and the release notes that accompany each release give the share of origins that have good LCP, CLS, INP and overall Core Web Vitals. Read across a few years, they're the best public record of how much the measuring stick moves. Three cases stand out, and Fig. 3 puts the two cleanest side by side.
The first is the metric swap itself. The February 2024 release, published on 12 March 2024, the day INP became stable, reported both numbers for the same month of data: 48.8% of origins had good Core Web Vitals using FID, and 45.6% using INP in its place. Same origins, same users, same month of data. Changing which responsiveness metric counted moved 3.2 percentage points of origins from passing to failing overnight.
The second runs the other way and is happening now. The September 2026 release, published on 13 October 2026, reports 55.9% of origins with good Core Web Vitals under the stable metrics, and 59.5% if soft navigations are included. The notes call that "an ↑ 6.4%", which is the relative change; in percentage points it's 3.6. The soft-navigation post explains where that comes from. Before, CrUX attributed everything in a single-page-app session to the landing page: one LCP for the cold load, and every interaction and layout shift across the session piled onto it. With soft navigations, each in-app view change that updates content and URL is its own navigation. The post says about 70% of origins have little or no soft navigation traffic (under 5% of page views) and shouldn't see a change, but sites with mostly soft navigations see large gains. On Android, origins with 75-100% soft navigations go from 26% good LCP to 87%, and the post says the Android INP pass rate for mostly-soft-navigation sites roughly doubles.
Neither change involved a single site deploying anything. If your SPA's pass rate jumps in mid-2027, check the date before crediting your last sprint: it may be the definition catching up with how your app works. The post is clear that once the experimental metrics graduate, Search Console switches to them, so the jump will show up where your stakeholders look.
The third case is a changelog item showing up in the aggregate. The April 2023 release reported a slight decrease in pass rates, especially for LCP, "due in part to" the change to ignore low-entropy images, and the May 2023 release attributed a further LCP and FCP decrease to the final rollouts of the same change and of a paint-timing correction. The Chrome 112 changelog explains the mechanism: images below 0.05 bits of image data per displayed pixel no longer count as LCP candidates, so a page that painted a low-content placeholder early now has its LCP at the real image, later. Nothing got slower. A placeholder stopped being counted as content.
INP had its own version in 2024. The Chrome 128 changelog excluded some scroll-starting interactions that Event Timing had been measuring by accident. The August 2024 CrUX release says that dropping pointerup events used for scrolling both improved INP and reduced its coverage, so some sites lost their INP value in CrUX altogether. Under the pass rule from earlier, a page that loses INP is judged on LCP and CLS alone.
Not every movement has an explanation, and the release notes are candid about that. The May 2026 release reports a regression, mostly on Android, with "a few suspicions" and nothing definitive, and the August 2026 release calls the continued INP regression a cause for concern without a definitive reason. If the people who run the dataset can't always attribute a change, you shouldn't assume your own dip is your fault, or that your own improvement is your doing.
Spec maturity, Baseline status and Core Web Vitals status are three separate axes
It's tempting to assume that a metric important enough to show up in Search Console must be a finished W3C standard. None of them is. On the W3C Web Performance Working Group's spec list, as of today, Largest Contentful Paint and the Event Timing API are Working Drafts, Navigation Timing Level 2 is a Working Draft, and the Long Animation Frames API is a First Public Working Draft. The latest published versions are the LCP Working Draft of 26 August 2026, the Event Timing Working Draft of 13 October 2026, the Navigation Timing Level 2 Working Draft of 1 September 2026 and the Long Animation Frames First Public Working Draft of 28 April 2026.
CLS is further from standardization still. The Layout Instability API is a Draft Community Group Report dated 17 December 2025, published by the Web Platform Incubator Community Group, and its status section says it is not a W3C Standard nor on the W3C Standards Track. A metric that was already in the set in the March 2021 snapshot of the Web Vitals page rests on an incubation document.
Cross-browser availability is a third axis again. As of today, webstatus.dev lists Largest Contentful Paint and Event Timing as Baseline newly available since 12 December 2025, when Safari shipped both. Layout Instability has limited availability: Chrome and Edge only. The CrUX and RUM guide adds that LCP and INP in Safari and Firefox can be measured quite differently from Chrome, which is why it recommends filtering RUM to Chrome users before comparing with CrUX. PRF-05 in this series looks at what that Baseline status does and doesn't let you compare.
So a metric can be stable in the program, a Working Draft or CG report in the standards process, and anywhere from Chromium-only to Baseline in browsers, all at once. None of those statuses implies the others. When someone asks "is CLS a standard?", the precise answer has three parts.
Honest reporting needs the long form, your own RUM and your own history
Here's what I'd put in place tomorrow if I owned the number.
Say the long form at least once. In any document where CWV is a goal, define "passes" as Google does: the 75th percentile of opted-in Chrome users' page loads over 28 days, per form factor, all three metrics good, INP only if present. An argument about a moved number goes nowhere until everyone agrees what the number is. It's never a property of the page; it's a property of a population visiting the page.
Run your own RUM, and record which Chrome measured it. The program page itself strongly recommends real-user monitoring, because CrUX doesn't give you per-pageview telemetry. The field that makes RUM useful against the changelogs is the browser version, because that's what lets you line your data up with changelog entries. Here's a minimal beacon using web-vitals 6.2.3, the current release on npm as of today.
It reads navigator.userAgentData.brands, which has limited availability per webstatus.dev (Chromium browsers only) and is marked experimental on MDN. That's acceptable here, since the comparison you care about is with CrUX, which is Chrome-only anyway; other browsers record null for the Chrome version.
import { onCLS, onINP, onLCP } from 'web-vitals';
// CrUX only counts Chrome, so record the Chrome major version.
// Matching "Google Chrome" also excludes Edge, which CrUX excludes.
function chromeMajor() {
const brands = navigator.userAgentData?.brands ?? [];
const chrome = brands.find((b) => b.brand === 'Google Chrome');
return chrome ? Number(chrome.version) : null;
}
function sendToAnalytics(metric) {
const body = JSON.stringify({
name: metric.name,
value: metric.value,
rating: metric.rating,
id: metric.id,
navigationType: metric.navigationType,
chrome: chromeMajor(),
mobile: navigator.userAgentData?.mobile ?? null,
// The URL the metric belongs to, which can differ from the
// current one once soft navigations are reported.
path: new URL(metric.navigationURL ?? location.href).pathname,
});
// sendBeacon returns false if it couldn't queue the request.
if (!navigator.sendBeacon('/analytics', body)) {
fetch('/analytics', { body, method: 'POST', keepalive: true });
}
}
onCLS(sendToAnalytics);
onINP(sendToAnalytics);
onLCP(sendToAnalytics);Two notes on that. The library's rating field is per page view, using the same good and poor thresholds as the program ([2500, 4000] for LCP, [200, 500] for INP, [0.1, 0.25] for CLS). A page view rated "good" doesn't mean the page passes; you still need the p75 across views. And navigationType will read soft-navigation once you opt in with reportSoftNavs: true, which the README says changes how the first page load is measured too. Decide that deliberately, and before May 2027 rather than after.
Keep the CrUX history yourself. The History API only keeps 40 weekly collection periods, about ten months, and updates each Monday around 04:00 UTC. Anything older means BigQuery's monthly tables. A small scheduled job that saves the weekly p75s gives you a record long enough to see a definition change coming and going. The request shape and response fields below are from the History API docs. It needs a global fetch, which Node.js has had without a flag since 18 and as a stable API since 21, and top-level await, which is why the file is .mjs.
// Keep the key in the environment, never in the repo.
const KEY = process.env.CRUX_API_KEY;
const ENDPOINT =
'https://chromeuxreport.googleapis.com/v1/records:queryHistoryRecord';
// The API returns dates as { year, month, day }.
const pad = (n) => String(n).padStart(2, '0');
const isoDate = ({ year, month, day }) => `${year}-${pad(month)}-${pad(day)}`;
export async function weeklyP75(origin, metric, formFactor = 'PHONE') {
const res = await fetch(`${ENDPOINT}?key=${KEY}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
origin,
formFactor,
metrics: [metric],
collectionPeriodCount: 40,
}),
});
if (!res.ok) throw new Error(`CrUX History API: ${res.status}`);
const { record } = await res.json();
const p75s = record.metrics[metric].percentilesTimeseries.p75s;
return record.collectionPeriods.map((period, i) => ({
// Each period is the 28 days ending on this date.
lastDate: isoDate(period.lastDate),
// null when the period had no eligible data; CLS comes as a string
p75: p75s[i] === null ? null : Number(p75s[i]),
}));
}
console.log(await weeklyP75('https://example.com', 'largest_contentful_paint'));The soft-navigation post says the experimental metrics are available in the History API too, so the same function should take experimental_largest_contentful_paint_with_softnavs as the metric name. The History API reference's list of allowed metric values doesn't include the new names yet, and I haven't run the query myself, so treat that as the post's claim until you've seen a response. If it works, saving both series now gives you a before-and-after for when Search Console switches.
Annotate, then be modest. When a weekly series moves, put three kinds of marks on the chart: your deploys, the Chrome stable dates from Chromium Dash for any changelog entry that says it affects field metrics, and the CrUX release notes. That's the method I'd planned to demonstrate with Chrome 147, and it's sound for entries that do touch CrUX. Remember the overlap, though: a change lands as a four-week slide, and gradual flag rollouts smear it further. Lining up a slide with a changelog entry is circumstantial evidence. Your RUM, split by Chrome version, is the stronger test, and even that isn't clean when a change rolls out by flag across versions.
Subscribe to the two records. Watch the Chromium changelog directory and read each month's CrUX release notes. They're how you tell "we regressed" from "the definition changed" when someone asks.
Set your own targets beyond "good". The thresholds were chosen so that at least 10% of origins could meet them in 2020 and 2022, and to suit mobile. They're a floor that applies to the whole web. If you have revenue tied to speed, set internal p75 targets below the thresholds and consider a higher percentile for the segments you care most about, and label those as your own metrics so nobody confuses them with Google's.
What we don't know
Whether the soft-navigation variants are a lifecycle stage of the existing metrics or new metrics. The CrUX post calls them experimental and targets May 2027 for graduation, and says Search Console will switch to them. The Web Vitals lifecycle, last updated in 2024, doesn't mention them, and describes the experimental phase as applying to prospective metrics rather than to new variants of stable ones. If graduating counts as a definition change to LCP, INP and CLS, it presumably uses up the year's one change for all three. An update to the lifecycle page, or a changelog entry when they graduate, would settle it.
What "won't change more than once per year" counts. I found no public log that separates the annual definition change from the "metric definition improvement" entries in the changelogs, no statement of whether the count is per metric or for the set, and none of when the year starts. 2025 and 2026 fit a one-per-metric reading; 2021 and 2023 don't. Without a published count, the promise can't be checked from outside, and neither can the quiet move from "less than 0.1" to "0.1 or less".
Whether a changelog item's rollout can be separated from site changes in CrUX. The changelogs give milestones, and some entries say they rolled out by flag across several versions. The History API is aggregated by form factor only, and I found no Chrome-version dimension in CrUX's documentation. Attribution from aggregate data is circumstantial by design; I'd want Google to publish per-change before-and-after pass rates, as it did for the FID to INP swap, to call any single case settled.
Which release date a changelog entry means. For Chrome 147 and 151 the detailed changelog pages give stable months that disagree with the Chromium Dash schedule by two months and one month respectively, and match each milestone's feature-freeze month instead. I've used Chromium Dash's stable dates, but I don't know what the changelog authors meant, and a correction to those pages would settle it.
Whether the thresholds have been re-derived. The achievability tables in the thresholds article date from April 2020 and May 2022. With 85.4% of origins now having good INP and 81.7% good CLS in the September 2026 release, at most 14.6% of origins can fall outside "good" for INP, and at most 18.3% for CLS, and the poor bands can only hold some part of that. The release notes don't publish poor shares, so I can't say whether those bands still catch the 10 to 30 percent of origins they were sized for. I found no statement on whether Google plans to revisit them.
What share of Chrome users CrUX sees. The methodology says Chrome doesn't publish the proportion of users who meet the eligibility criteria. Every claim about "Chrome users" in this piece means "eligible Chrome users", a population whose size and makeup aren't public.
Notes
-
The article's text says 4 seconds would classify "roughly 26% of phone origins, and 21% of desktop origins" as poor, but the table directly above it gives 19% for desktop at 4 seconds. The phone figure matches; I'd trust the table for desktop, but the article doesn't say which is right. ↩︎
References
- Web Vitals. web.dev (Google)>. Accessed .
- How the Core Web Vitals metrics thresholds were defined. web.dev (Google)>. Accessed .
- Interaction to Next Paint (INP). web.dev (Google)>. Accessed .
- Why is CrUX data different from my RUM data?. web.dev (Google)>. Accessed .
- Cumulative Layout Shift (CLS). web.dev (Google)>. Accessed .
- Web Vitals (Wayback Machine snapshot, 4 March 2021). Internet Archive>. Accessed .
- Interaction to Next Paint becomes a Core Web Vital on March 12. web.dev (Google)>. Accessed .
- Interaction to Next Paint is officially a Core Web Vital. web.dev (Google)>. Accessed .
- Introducing INP to Core Web Vitals. Google Search Central Blog>. Accessed .
- Experimental soft navigation metrics now available in CrUX. web.dev (Google)>. Accessed .
- Web Vitals Changelogs. Chromium>. Accessed .
- Largest Contentful Paint Changelog. Chromium>. Accessed .
- Cumulative Layout Shift Changelog. Chromium>. Accessed .
- Interaction to Next Paint Changelog. Chromium>. Accessed .
- First Input Delay Changelog. Chromium>. Accessed .
- Largest Contentful Paint Changes in Chrome 147. Chromium>. Accessed .
- Largest Contentful Paint Changes in Chrome 151. Chromium>. Accessed .
- Largest Contentful Paint change in Chrome 112 to ignore low-entropy images. Chromium>. Accessed .
- Interaction to Next Paint Changes in Chrome 128. Chromium>. Accessed .
- Cumulative Layout Shift Changes in Chrome 145. Chromium>. Accessed .
- Chromium Dash release schedule. Chromium>. Accessed .
- Web Performance Working Group. W3C WebPerf WG>. Accessed .
- Largest Contentful Paint - W3C Working Draft, 26 August 2026. W3C>. Accessed .
- Event Timing API - W3C Working Draft, 13 October 2026. W3C>. Accessed .
- Long Animation Frames API - W3C First Public Working Draft, 28 April 2026. W3C>. Accessed .
- Navigation Timing Level 2 - W3C Working Draft, 1 September 2026. W3C>. Accessed .
- Layout Instability API - Draft Community Group Report, 17 December 2025. WICG>. Accessed .
- CrUX methodology. Chrome for Developers>. Accessed .
- CrUX release notes. Chrome for Developers>. Accessed .
- CrUX History API. Chrome for Developers>. Accessed .
- Largest Contentful Paint - feature status. Web Platform Status (webstatus.dev)>. Accessed .
- Event Timing - feature status. Web Platform Status (webstatus.dev)>. Accessed .
- Layout Instability - feature status. Web Platform Status (webstatus.dev)>. Accessed .
- User-Agent Client Hints - feature status. Web Platform Status (webstatus.dev)>. Accessed .
- NavigatorUAData: brands property. MDN>. Accessed .
- Global objects: fetch. Node.js>. Accessed .
- web-vitals 6.2.3 README. GoogleChrome (npm package web-vitals)>. Accessed .