# Core Web Vitals Tracker - CrUX History & Benchmark (`prevailing_glow/core-web-vitals-tracker`) Actor

Benchmark Core Web Vitals (LCP, INP, CLS) of your sites vs competitors from real Chrome user data: pass/fail, 40-week CrUX history, trends, regression alerts.

- **URL**: https://apify.com/prevailing_glow/core-web-vitals-tracker.md
- **Developed by:** [Dave West](https://apify.com/prevailing_glow) (community)
- **Categories:** SEO tools, Developer tools, E-commerce
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 site checks

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Core Web Vitals Tracker - CrUX History & Benchmark

Compare the **real-user Core Web Vitals** of your sites with your competitors': **LCP, INP and CLS** (plus FCP and TTFB) from Google's **Chrome UX Report (CrUX)**, the same field data Google uses for the page experience signal and shows in Search Console and PageSpeed Insights.

Paste your sites and a few competitors. For every site and device (phone, desktop) you get one clean row with the **p75 values**, **good / needs improvement / poor shares**, **pass/fail on Google's thresholds**, the **Core Web Vitals assessment**, a **rank among all sites**, the **change vs 4 and 12 weeks ago** from up to **40 weeks of CrUX history**, and **regression alerts with the week they started**. You also get an **HTML report** with a competitor benchmark table and trend sparklines, and optionally a **Lighthouse lab audit** (performance score and top 5 fixes) for a few URLs.

No browser, no crawling: about **$8 per 1,000 sites** with history, or $3 without. Sites without CrUX data are free.

### What you get beyond the raw CrUX API

| | Raw CrUX API | This Actor |
|---|---|---|
| Current p75 + distributions | per query, nested JSON | one flat row per site × device, CSV/Excel ready |
| Pass/fail | - | per metric on Google's thresholds + the CWV assessment (incl. the "LCP + CLS when INP has too little data" rule) |
| Competitor benchmark | - | rank, per-metric rank, gap to the best site and to your best competitor |
| History | 40 weekly arrays | change vs 4 / 12 weeks (ms and %), trend label, weeks passing CWV, pass/fail 4 and 12 weeks ago |
| Regressions | - | regression episodes with week dates, before/after p75, status change, "ongoing" flag |
| Report | - | HTML benchmark with inline SVG sparklines, your sites highlighted |
| www vs non-www | 404 if you pick the wrong one | tries the other variant automatically |
| Quota handling | 150 queries/min, HTTP 429 | built-in rate limiter, backoff and retries |

### Use cases

- **Competitor benchmark for SEO and ecommerce teams**: are we faster than the competition on mobile? Where is the gap largest?
- **Monitoring and alerting**: schedule a weekly run (the CrUX history updates every Monday) and get flagged regressions such as "INP +25% since 2026-08-08 (good → needs improvement)".
- **Agency reporting**: one table and one HTML report for a client and its top 5 competitors, with trend lines over 40 weeks.
- **Release impact**: did the new site, theme or tag manager setup make LCP or CLS worse? See the week it changed.
- **Prospecting**: find sites in a niche that fail Core Web Vitals.

### Quick start

1. Add your site(s) under **Your sites** and competitors under **Competitor sites** (`https://www.example.com` or just `example.com`).
2. Keep both devices and **Include 40-week history** on.
3. Run, then open the **HTML report** or download the dataset (views: Benchmark, Distributions, Trends & regressions, Lab audits).

Leave everything empty for a **free preview** on four example sites.

### Input example

```json
{
    "urls": ["https://www.bol.com"],
    "competitors": ["https://www.coolblue.nl", "https://www.amazon.nl", "https://www.mediamarkt.nl"],
    "formFactors": ["PHONE", "DESKTOP"],
    "mode": "origin",
    "includeHistory": true,
    "labAudit": false,
    "regressionThresholdPct": 15
}
```

- **Level**: `origin` (whole site, what Search Console uses, many more sites have data) or `url` (one exact page; needs a lot of traffic).
- **Lab audit** (optional): `labAudit: true` runs PageSpeed Insights for up to `maxLabAudits` URLs (default 5: the first sites' homepages, or your own `labAuditUrls`), `labStrategy` mobile / desktop / both.
- **Regression alerts**: `regressionThresholdPct` (default 15), `regressionMinLcpMs` (150), `regressionMinInpMs` (25), `regressionMinCls` (0.05).
- **Your own Google API key** (optional, encrypted secret field): see below.

### Output

One row per site × device. Real example (October 2026, shortened):

```json
{
  "recordType": "field",
  "site": "https://www.bol.com",
  "role": "own",
  "queried": "https://www.bol.com",
  "formFactor": "PHONE",
  "collectionPeriodFrom": "2026-09-01",
  "collectionPeriodTo": "2026-09-28",
  "lcpP75Ms": 1302, "inpP75Ms": 152, "clsP75": 0, "fcpP75Ms": 1194, "ttfbP75Ms": 221,
  "lcpStatus": "good", "inpStatus": "good", "clsStatus": "good",
  "lcpPass": true, "inpPass": true, "clsPass": true,
  "cwvPass": true,
  "cwvAssessedOn": "LCP, INP, CLS",
  "cwvScore": 92.3,
  "rank": 1, "rankOf": 4, "lcpRank": 2, "inpRank": 2, "clsRank": 1,
  "lcpGapToBestMs": 47, "inpGapToBestMs": 43, "clsGapToBest": 0,
  "lcpGoodPct": 95.9, "lcpNeedsImprovementPct": 3.1, "lcpPoorPct": 1,
  "inpGoodPct": 84.7, "inpNeedsImprovementPct": 12.5, "inpPoorPct": 2.8,
  "historyWeeks": 40, "historyFrom": "2025-12-27", "historyTo": "2026-09-26",
  "lcpChange4wMs": 24, "lcpChange4wPct": 1.9, "lcpChange12wMs": 46, "lcpChange12wPct": 3.7,
  "inpChange4wMs": -17, "inpChange12wPct": -6.1,
  "lcpTrend": "stable", "inpTrend": "stable", "clsTrend": "stable",
  "cwvPass12wAgo": true, "cwvPassWeeks": 40, "cwvAssessedWeeks": 40,
  "regressionCount": 1,
  "regressionSummary": "LCP 2026-02-07 +25.8%",
  "recentCwvRegression": false,
  "regressions": [{ "metric": "LCP", "weekEnding": "2026-02-07", "comparedToWeekEnding": "2026-01-10", "fromP75": 1134, "toP75": 1427, "changePct": 25.8, "statusFrom": "good", "statusTo": "good", "ongoing": false }],
  "history": { "weeks": ["2025-12-27", "...", "2026-09-26"], "lcp": [1094, "...", 1303], "inp": ["..."], "cls": ["..."], "fcp": ["..."], "ttfb": ["..."] },
  "dataSource": "Data: Chrome UX Report by Google, CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/)",
  "chargedEvents": ["site-check", "history-report"]
}
```

Real results from the same run (phone, origin level, 28 days to 2026-09-28):

| Rank | Site | CWV | LCP p75 | INP p75 | CLS p75 | Flagged regressions |
|---|---|---|---|---|---|---|
| 1 | bol.com | Passed | 1.30 s | 152 ms | 0.00 | LCP 2026-02-07 +25.8% |
| 2 | coolblue.nl | Passed | 1.45 s | 180 ms | 0.00 | INP 2026-08-08 +25.2% |
| 3 | amazon.nl | Passed | 1.26 s | 109 ms | 0.09 | CLS 2026-09-26 +125% (ongoing) |
| 4 | mediamarkt.nl | Failed | 2.36 s | 339 ms | 0.09 | INP 2026-05-23 +21.3% (needs improvement → poor) |

Field names: all p75s are in milliseconds (`...Ms`) except CLS (unitless). Changes are "latest minus earlier", so **positive = worse** for every metric. `cwvScore` is the average share of good experiences over LCP, INP and CLS and is used, after pass/fail, for the rank. Site-level charges are listed in `chargedEvents` of the site's first row.

Sites without CrUX data get a row with `noData: true` and a `noDataReason` (not charged). With lab audits on, the dataset also contains `recordType: "lab"` rows: `performanceScore`, `labLcpMs`, `labTbtMs`, `labCls`, `labFcpMs`, `labSpeedIndexMs`, `topOpportunities` (top 5 Lighthouse opportunities with estimated savings) and `opportunities` (structured).

The key-value store also contains:

- `OUTPUT.html`: the HTML report (benchmark per device with sparklines, your sites vs the best competitor, regressions, lab audits).
- `SUMMARY`: the same as JSON: benchmark, best site per metric, own sites vs best competitor (gaps in ms), all regressions, no-data sites, API usage and charges.

### How regressions are detected

CrUX history has one 28-day window per week. For LCP, INP and CLS, each week is compared with the window **4 weeks earlier** (so the two windows do not overlap). A week is flagged when the p75 is worse by at least `regressionThresholdPct` percent **and** the minimum change (150 ms LCP, 25 ms INP, 0.05 CLS), or when the metric crossed into a worse status (good → needs improvement → poor) by at least half the minimum change. Consecutive flagged weeks are one episode, reported at its first week with the worst value reached; `ongoing` means it is still regressed in the latest week. The defaults were tuned on 40 weeks of history of 49 large Dutch sites to flag real shifts rather than week-to-week noise; lower them for more sensitive alerts.

### Your own Google API key (optional, recommended for big runs)

The Actor comes with its own key, which Google limits to **150 CrUX queries per minute shared by all users of this Actor**. It paces itself at 120/min and retries on HTTP 429, so normal runs simply work. On the shared key a run is limited to 250 sites.

For large or frequent runs, paste your own free key in **Google API key**: create a project in Google Cloud Console, enable the **Chrome UX Report API** (and the **PageSpeed Insights API** if you use lab audits), create an API key. The field is a secret input: it is stored encrypted, used only for your run, never logged or written to the output, and it replaces the Actor's key. With your own key a run can check up to 2,000 sites at up to 150 queries/min. The CrUX API is free; Google does not sell more quota.

### Speed

Each site needs about 4 queries with both devices and history (2 without history). At 120 queries/min that is **~30 sites per minute**: 50 sites took 96 s in testing, 1,000 sites take about 35 minutes. Lab audits take 10-60 s each (3 in parallel).

### Pricing

Pay per event, no subscription:

| Event | Price | When |
|---|---|---|
| Site check (`site-check`) | **$0.003** | per site with CrUX data, phone and desktop together |
| History report (`history-report`) | **$0.005** | per site whose 40-week history was returned and analysed |
| Lab audit (`lab-audit`) | **$0.004** | per successful PageSpeed Insights audit (per URL and device) |

- 1,000 sites with history: **about $8**; without history: $3. Five competitors with history: $0.05.
- **Free**: sites without CrUX data (`noData`), failed queries, failed lab audits, and the preview run with empty input.
- The run never exceeds your **maximum total charge per run**: sites are only queried while the budget can cover them, and it stops cleanly when the limit is reached.

### Limitations

- **Only sites with enough Chrome traffic are in CrUX.** Small sites, intranets and pages behind a login often have no data (returned free with `noData: true`). URL-level data needs much more traffic than origin-level data.
- **Data lags**: the current 28-day window ends about 2 days ago; the history updates weekly on Mondays.
- **Field data is a 28-day rolling window**, so a fix shows up gradually over 4 weeks; regression dates are the first week the change crossed the threshold, not the release date.
- **Tablet** is not reported (rarely has enough data); phone and desktop are.
- **Lab audits** are a single simulated Lighthouse load from Google's servers and can differ from real-user data.
- The shared key's quota is shared by all users; at busy times runs can be slowed by retries. Add your own key for guaranteed throughput.

### FAQ

**Why does my site show no data when PageSpeed Insights shows data?** PageSpeed Insights falls back to origin data when a URL has none. Use `mode: "origin"`, and check that you entered the origin your users land on (`https://www.example.com` vs `https://example.com`); the Actor tries the other variant automatically.

**Why does INP say "n/a" or the assessment say "LCP, CLS"?** Some origins have too few interactions for INP on a device. Google then assesses Core Web Vitals on LCP and CLS only, and so does this Actor.

**How often should I run it?** Weekly (Monday or later) for history and regressions; daily runs only change the current 28-day values.

**Is this the same data as Search Console?** Same source (CrUX), but Search Console groups similar URLs; origin-level CrUX is the closest site-wide comparison and the fairest way to compare competitors.

**Can I run it on a schedule or via API?** Yes, like every Apify Actor: schedule it in the Console, call it via the API, or connect it to Make, Zapier, n8n or Google Sheets.

### Data source and attribution

**Data: Chrome UX Report by Google, CC BY 4.0** ([license](https://creativecommons.org/licenses/by/4.0/)). Field data comes from the [Chrome UX Report](https://developer.chrome.com/docs/crux) via the CrUX API and CrUX History API; lab data from the PageSpeed Insights API v5 (Lighthouse). Ranking, trends, regression detection and the report are computed by this Actor. Every field row carries the attribution in `dataSource`, and the HTML report and SUMMARY include it. This Actor is not affiliated with or endorsed by Google.

### Support

Something looks wrong or you need another metric (e.g. LCP subparts, navigation types)? Open an issue on the Actor's Issues tab with your input and what you expected.

# Actor input Schema

## `urls` (type: `array`):

Origins or URLs of your own sites, one per line, e.g. <code>https://www.example.com</code> or <code>example.com</code>. Marked as <b>you</b> in the benchmark. Each site with data is one billable site check (phone and desktop together).

## `competitors` (type: `array`):

Competitor origins or URLs, one per line. Ranked together with your sites.

## `formFactors` (type: `array`):

Which devices to report. Each device is a separate row per site. Phone and desktop are billed together as one site check.

## `mode` (type: `string`):

<b>Origin</b>: all pages of the site together (what Search Console and most benchmarks use; much more sites have data). <b>URL</b>: only the exact page you entered (e.g. a landing page or product page; needs a lot of traffic to have data).

## `includeHistory` (type: `boolean`):

Load up to 40 weekly data points from the CrUX History API: change vs 4 and 12 weeks ago, trend, weeks passing, regression alerts with dates, and sparklines in the report. Billed as one extra history report per site.

## `tryWwwVariant` (type: `boolean`):

CrUX stores data under the origin users actually land on. If <code>example.com</code> has no data, <code>www.example.com</code> is tried (and vice versa). Origin level only.

## `labAudit` (type: `boolean`):

Also run a Lighthouse lab test via the PageSpeed Insights API for a few URLs: performance score, lab LCP / TBT / CLS and the top 5 opportunities. Takes 10-60 s per URL. Billed per successful audit.

## `labAuditUrls` (type: `array`):

Pages to test, one per line. Leave empty to test the homepage of the first sites in the list (up to the limit below).

## `labStrategy` (type: `string`):

Mobile (Google's default, slow 4G + mid-range phone), desktop, or both (two audits per URL).

## `maxLabAudits` (type: `integer`):

Upper limit on lab-audited URLs per run.

## `regressionThresholdPct` (type: `integer`):

Flag a regression when the p75 of LCP, INP or CLS is this many percent worse than 4 weeks earlier (non-overlapping 28-day windows). A metric that crosses into a worse status (good → needs improvement → poor) is always flagged.

## `regressionMinLcpMs` (type: `integer`):

Ignore LCP changes smaller than this. Crossing into a worse status (e.g. good → needs improvement) is flagged from half this change.

## `regressionMinInpMs` (type: `integer`):

Ignore INP changes smaller than this.

## `regressionMinCls` (type: `string`):

Ignore CLS changes smaller than this (e.g. <code>0.05</code>).

## `googleApiKey` (type: `string`):

Optional. By default the Actor uses its own key, which has 150 queries/min shared by all users. For big or scheduled runs, create a free key in Google Cloud with the <b>Chrome UX Report API</b> (and <b>PageSpeed Insights API</b> for lab audits) enabled and paste it here: it is stored encrypted, used only for this run and replaces ours. Allows up to 2,000 sites per run.

## `maxQueriesPerMinute` (type: `integer`):

Pace of CrUX API queries. Google allows 150/min per Google Cloud project. Values above 120 are only used with your own key.

## Actor input object example

```json
{
  "urls": [
    "https://www.bol.com"
  ],
  "competitors": [
    "https://www.coolblue.nl",
    "https://www.amazon.nl",
    "https://www.mediamarkt.nl"
  ],
  "formFactors": [
    "PHONE",
    "DESKTOP"
  ],
  "mode": "origin",
  "includeHistory": true,
  "tryWwwVariant": true,
  "labAudit": false,
  "labStrategy": "mobile",
  "maxLabAudits": 5,
  "regressionThresholdPct": 15,
  "regressionMinLcpMs": 150,
  "regressionMinInpMs": 25,
  "regressionMinCls": "0.05",
  "maxQueriesPerMinute": 120
}
```

# Actor output Schema

## `report` (type: `string`):

Competitor benchmark per device with sparklines, regressions and lab audits.

## `benchmark` (type: `string`):

One row per site and device: rank, pass/fail, p75s.

## `results` (type: `string`):

Full rows incl. distributions, trends, regressions and weekly history.

## `summary` (type: `string`):

Benchmark, your sites vs best competitor, regressions.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "urls": [
        "https://www.bol.com"
    ],
    "competitors": [
        "https://www.coolblue.nl",
        "https://www.amazon.nl",
        "https://www.mediamarkt.nl"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("prevailing_glow/core-web-vitals-tracker").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "urls": ["https://www.bol.com"],
    "competitors": [
        "https://www.coolblue.nl",
        "https://www.amazon.nl",
        "https://www.mediamarkt.nl",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("prevailing_glow/core-web-vitals-tracker").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "urls": [
    "https://www.bol.com"
  ],
  "competitors": [
    "https://www.coolblue.nl",
    "https://www.amazon.nl",
    "https://www.mediamarkt.nl"
  ]
}' |
apify call prevailing_glow/core-web-vitals-tracker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,prevailing_glow/core-web-vitals-tracker"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/pHDZToTvmJsk8iucZ/builds/4Qb5WTixV9wwAe6cR/openapi.json
