A new cookie banner shipped on the 3rd. It pushed the main content down after load. Search Console's Core Web Vitals report started showing "Poor" CLS URLs around the 20th, and the team only connected it to the banner at the end of the month. By then nobody remembered what had shipped on the 3rd.

Field data is the truth about real users. It's also a slow-moving average that reports problems weeks late.

TL;DR

  • Core Web Vitals field data (CrUX) is a 28-day rolling window of real Chrome users. A regression appears gradually and disappears gradually.
  • The CrUX API lets you pull LCP, INP and CLS at the 75th percentile for a URL or origin, for free, with a script.
  • Use field data to know whether there's a problem, and lab tests in CI to know which deploy caused it.
  • My opinion: lab scores are not the goal, but a lab check that blocks obvious regressions is the only way to get feedback in hours instead of weeks.

Why the delay happens

Each CrUX number is the 75th percentile across the previous 28 days. If a deploy makes LCP worse for everyone today, only 1/28 of the window reflects it tomorrow. The p75 moves a little each day and may take a week or two to cross a threshold. After a fix, it takes just as long to recover.

So "Search Console shows the problem" and "the problem started" can be weeks apart.

Pulling field data with the CrUX API

Get an API key in Google Cloud (CrUX API enabled), then:

curl -s "https://chromeuxreport.googleapis.com/v1/records:queryRecord?key=$CRUX_KEY" \ -H 'Content-Type: application/json' \ -d '{"origin": "https://example.com", "formFactor": "PHONE", "metrics": ["largest_contentful_paint","interaction_to_next_paint","cumulative_layout_shift"]}' \ | jq '.record.metrics | map_values(.percentiles.p75)' 
{ "cumulative_layout_shift": "0.21", "interaction_to_next_paint": 180, "largest_contentful_paint": 2900 } 

Swap origin for url to query a single page. Low-traffic URLs often have no data, in which case the API returns 404 and you fall back to origin-level numbers.

Track it daily so you see the slope

A single reading tells you little. The slope tells you a lot:

import json, os, datetime, requests KEY = os.environ["CRUX_KEY"] TARGETS = ["https://example.com", "https://example.com/pricing"] THRESH = {"largest_contentful_paint": 2500, "interaction_to_next_paint": 200, "cumulative_layout_shift": 0.1} def p75(target): body = {"formFactor": "PHONE", "metrics": list(THRESH)} body["origin" if target.count("/") == 2 else "url"] = target r = requests.post(f"https://chromeuxreport.googleapis.com/v1/records:queryRecord?key={KEY}", json=body, timeout=20) if r.status_code == 404: return None m = r.json()["record"]["metrics"] return {k: float(m[k]["percentiles"]["p75"]) for k in THRESH if k in m} today = datetime.date.today().isoformat() with open("crux-history.jsonl", "a") as f: for t in TARGETS: vals = p75(t) f.write(json.dumps({"date": today, "target": t, "p75": vals}) + "\n") if vals: bad = [k for k, v in vals.items() if v > THRESH[k]] print(t, vals, "OVER: " + ", ".join(bad) if bad else "ok") 

Alert on direction, not just thresholds: if CLS p75 rose on 5 of the last 7 days, something changed, even if it's still "good". The API also has a queryHistoryRecord endpoint with weekly data points going back several months, which is handy for matching a slope to your deploy history.

The lab gate: feedback on the same day

Field data tells you something's wrong. A lab test in CI tells you which pull request did it. With Lighthouse CI:

{ "ci": { "collect": { "url": ["http://localhost:3000/", "http://localhost:3000/pricing"], "numberOfRuns": 3 }, "assert": { "assertions": { "largest-contentful-paint": ["error", { "maxNumericValue": 3000 }], "cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }], "total-blocking-time": ["warn", { "maxNumericValue": 300 }] } } } } 

Lab tests can't measure INP (no real user interactions), so total blocking time is the usual proxy. Thresholds should be loose enough to tolerate noise and tight enough to catch a 1-second LCP regression. Run 3 times and use the median.

What usually causes regressions

From most to least common in the regressions worth chasing:

  • third-party scripts: tag managers, chat widgets, consent banners (CLS and INP)
  • hero images losing width/height, or switching to lazy loading above the fold (CLS, LCP)
  • web fonts swapping late (CLS)
  • a new client-side hydration step on content that used to be static (LCP, INP)

Several of these are invisible in raw HTML and only appear after rendering, which is the same blind spot covered in the raw vs rendered HTML post.

Put it in your weekly routine

The daily CrUX log and the CI gate do the heavy lifting. A weekly glance at the trend fits naturally into a broader checklist like A weekly technical SEO check routine for agency client sites. After a migration, watch it more closely, as described in How to monitor SEO after a site migration. Tools like seotto.com cover on-page changes like titles, robots and canonicals, so pairing them with this performance trend gives you most of the "what changed after the deploy?" picture.

My take

Teams argue about whether to optimise for lab or field scores. It's the wrong argument. Field data is the outcome, lab data is the early warning. Without a lab gate you learn about regressions a month late. Without field data you optimise a synthetic number nobody experiences.

Over to you

Do you gate deploys on Lighthouse or web-vitals budgets, or only watch field data? And which third-party script has caused you the worst regression? My bet is the consent banner.