# Core Web Vitals Benchmark: Page Speed Competitors, CrUX (`solidfetch/crux-competitor-benchmark`) Actor

Core Web Vitals benchmark for page speed competitors, from real Chrome UX Report (CrUX) data: LCP, INP, CLS at p75, pass or fail and weekly history for your site and rivals. A bulk PageSpeed Insights alternative, no API key needed. Pay only for sites with data.

- **URL**: https://apify.com/solidfetch/crux-competitor-benchmark.md
- **Developed by:** [Solid Fetch](https://apify.com/solidfetch) (community)
- **Categories:** SEO tools, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.00 / 1,000 site with data

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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 Benchmark: Page Speed Competitors from the Chrome UX Report

Compare the real-user site speed of your client with its competitors. Give the actor a list of sites and it returns one row per site from the official Chrome UX Report (CrUX) API: the 75th percentile and the share of good, needs-improvement and poor page loads for Largest Contentful Paint, Interaction to Next Paint, Cumulative Layout Shift, First Contentful Paint and Time to First Byte, a pass or fail verdict for Core Web Vitals, and a rank by LCP within your list (in the run summary). Optional weekly history shows whether a site is getting faster or slower.

The numbers are field data from Chrome users, the same data Google uses for the page experience signal. They are not a lab test of a single page load.

### What people use it for

- **A client versus competitors table.** Put the client and four rivals in `urls` and get one sheet: who is fastest on mobile, who passes Core Web Vitals, who is slowest on interaction.
- **Monthly agency reports.** Run it on a schedule with `includeHistory` on and chart 25 weeks of p75 per metric.
- **Prospecting.** Run a list of leads and keep the ones that fail LCP or INP.
- **Checking a fix.** Compare the same site before and after a release by the `collectionPeriod` dates and the history arrays.

### How to use it

You don't need an API key: the actor uses its own Chrome UX Report API key by default. That key's quota (150 queries per minute) is shared by everyone who runs the actor, so for long lists or frequent schedules you can paste your own free key in `apiKey`: in the Google Cloud console enable "Chrome UX Report API" for a project and create an API key under Credentials. Your key is stored as a secret input and is never written to the log, the dataset or error reports.

```json
{
    "urls": ["client-site.com", "competitor-a.com", "https://www.competitor-b.com/"],
    "level": "origin",
    "formFactor": "PHONE",
    "includeHistory": true,
    "historyPeriods": 25
}
```

### Input

| Field | What it does |
|---|---|
| `urls` | Domains, origins or page URLs, up to 200. A bare domain becomes `https://<domain>`. With `level` origin, a page URL is cut down to its origin, and when CrUX has no data for that origin the actor tries up to two sibling origins, only after a 404: the https version of the same host (for an `http://` input), then the same host with or without `www.` (https). The first one with data is reported in `queried` and the row gets `queriedFallback: true`, so a URL copied from the browser (`https://www.allegro.pl/kategoria/telefony`) or an `http://` one still finds the site. A sibling that turns out to be a site another input already gave is a free repeat. A list entry with several sites (`"web.dev, wp.pl"`) is split at commas and line breaks into separate inputs, and the run log says so once. A comma inside a page path or query (`https://shop.com/c/shoes,boots`) stays part of that URL unless a host name (`b.com/x`) or a space follows the comma. An empty entry gets its own free row with the error `empty input`. |
| `level` | `origin` (default) uses data of all pages of the site. `url` queries each page on its own, which has data only for pages with enough visits. |
| `formFactor` | `ALL` (default), `PHONE`, `DESKTOP` or `TABLET`. |
| `includeHistory` | Adds the p75 per collection period from the CrUX History API. Default off. |
| `historyPeriods` | Number of periods for the history, 1 to 40, default 25. |
| `apiKey` | Optional. Your own Chrome UX Report API key (secret). Left empty, the actor's own key is used. |

An unknown field name stops the run, so a typo cannot turn into a run that ignores a setting.

### Output

One row per input, in input order.

| Field | Meaning |
|---|---|
| `input` | The text you gave. |
| `level`, `queried`, `formFactor` | What was asked: the normalised origin or URL, and the device filter. `queried` is null for an invalid input. |
| `queriedFallback` | true when CrUX had no data for the origin as typed and `queried` is a sibling origin (https, or with or without `www.`) that has. Otherwise false. |
| `collectionPeriod` | `firstDate` and `lastDate` of the 28-day window the numbers cover. |
| `largest_contentful_paint`, `interaction_to_next_paint`, `cumulative_layout_shift`, `first_contentful_paint`, `experimental_time_to_first_byte` | Each is `{ p75, good, needsImprovement, poor }` or null when CrUX has no data for that metric. LCP, INP and TTFB are in milliseconds, CLS has no unit. The three shares add up to about 1. |
| `passesCoreWebVitals` | true when LCP p75 <= 2500, INP p75 <= 200 and CLS p75 <= 0.1. false if any is over. null when any of the three is missing. |
| `duplicateOf` | null, or the earlier input whose result this row repeats. Two inputs that normalise to the same site (for example `web.dev` and `https://web.dev/x` at level origin) are queried once and charged once; the repeat is saved free. |
| `history` | With `includeHistory`: `collectionPeriods` (oldest first) and `p75` arrays per metric, with null for a period without data. |
| `trend` | With `includeHistory`: per metric `{ trend, trendPercent }`, the mean p75 of the last 4 weekly periods against the 4 before. `improving` or `worsening` when it moved by more than 5 percent (lower is better for every metric), otherwise `stable`; `trendPercent` is the change in percent (null when the earlier mean is 0). A metric is null with fewer than 8 periods (`historyPeriods` 8 or more) or fewer than 2 values in a window. |
| `lcpBreakdown` | Why LCP is slow: p75 in ms of `ttfb`, `loadDelay`, `loadDuration` and `renderDelay`, the four subparts of the LCP of image elements, from the same CrUX call. Each p75 is taken on its own, so the four do not add up to the LCP p75. Null when CrUX has no subpart data (for example a site whose LCP is mostly text). Not part of the history. |
| `lcpResourceType` | Share of page loads by the type of the LCP element, such as `{ "text": 0.72, "image": 0.28 }`. Null when CrUX has none. Read it with `lcpBreakdown`: the subparts describe only the image share. |
| `bestInRun`, `gapToBest`, `wins` | The comparison inside your list, see below. |
| `historyError` | Why history is missing although the row has data, otherwise null. |
| `error` | null, or the reason: `no data for this origin`, `no data for this url`, `invalid url: ...`, `empty input`, `quota: ...`, an API error, or `not checked` after repeated failures. |
| `source` | Attribution, see below. |

Example row (shortened):

```json
{
    "input": "web.dev",
    "level": "origin",
    "queried": "https://web.dev",
    "formFactor": "PHONE",
    "collectionPeriod": { "firstDate": "2026-09-14", "lastDate": "2026-10-11" },
    "largest_contentful_paint": { "p75": 1800, "good": 0.82, "needsImprovement": 0.12, "poor": 0.06 },
    "passesCoreWebVitals": true,
    "error": null,
    "source": "Chrome UX Report (CrUX) by Google, CC BY 4.0, https://developer.chrome.com/docs/crux"
}
```

The values above only show the shape.

#### Comparison inside the run

For LCP, INP and CLS the actor finds the best site of your list (lowest p75; the first one on a tie; a metric that fewer than two sites have gets no best). `SUMMARY` holds it as `bestInRun`: `{ "largest_contentful_paint": { "input": "a.com", "p75": 1800 }, ... }`. Rows carry the same `bestInRun`, plus `gapToBest` per metric (`{ absolute, percent }`, percent of the best value, null when the best value is 0) and `wins`, the list of metrics where the row is best (ties included). The comparison costs no extra requests.

Rows are saved as they are checked, so a row cannot know the best site of the run before the whole list is queried. The actor therefore chooses by list size:

- **50 inputs or fewer:** all inputs are queried first, then the rows are saved in input order, each with its comparison. Nothing is charged before the last query. If the run stops halfway (for example the key is rejected), the rows already queried are still saved and charged before the run fails, and their comparison covers the inputs checked so far.
- **More than 50 inputs:** rows are saved one by one as before, so a stop keeps them. Their `bestInRun`, `gapToBest` and `wins` are null (a gap against the best site known so far would change with every row). Use `bestInRun` in `SUMMARY` and compute the gaps from the dataset.

An input with no data, an invalid input or a failed one has these fields null. A repeated input gets the same comparison as the input it repeats.

#### LCP rank

The LCP ranking of your list (1 for the lowest p75, equal values share a rank) is not a column of the dataset, because rows are saved one by one as they are checked. It is in the `ranking` list of the run's key-value store record `SUMMARY`, which also holds the inputs without data, the repeated and the failed ones. Rows are saved as they are checked. If the run stops (for example the key is rejected half way), the rows already saved stay in the dataset and are charged, and the failure message says how many. A run that restarts continues after the saved inputs and does not charge them again.

### Pricing

Pay per event, prices on the Pricing tab.

| Event | Charged for |
|---|---|
| `domain-checked` | Each row that has CrUX data. |

Not charged: rows with no data for the site, invalid and empty inputs, quota or API errors, inputs that were not checked, and anything past your maximum cost per run. History is part of the same row, not a second charge.

The final status line counts bad input apart from API trouble, for example `12 of 15 inputs had data, 1 had no CrUX data, 3 invalid, 2 failed.` Invalid (including empty) inputs are mistakes in the list; failed ones are quota, API or network errors.

### Limits

- **No data is normal.** CrUX only has sites and pages that Chrome users visit often enough. Small sites and most single pages return 404 from the API, which is reported as `no data for this origin` or `no data for this url` and is free. Origin level is the one that works for most sites.
- **Quota.** The API allows 150 queries per minute per Google Cloud project, free of charge. The actor sends at most two requests a second, retries 429 and 5xx answers with a wait of 5, 10, 20 and 40 seconds (and honours Retry-After), and does not retry a 404. If three inputs in a row fail that way (429 or 5xx after the retries, network errors, timeouts), the rest are marked `not checked`; a run where nothing could be checked because of such failures ends as failed instead of burning more of your quota. Using the same key in other tools at the same time counts against the same quota.
- **Rejected key.** A wrong key, a key without the API enabled or a key with a restriction that excludes this call stops the run with the reason from Google. Nothing is saved or charged.
- **Freshness.** CrUX updates daily and each number covers the latest 28 days, so two runs on the same day return the same values.
- **Form factor.** `ALL` combines devices. A site can have phone data and no desktop data.
- **LCP breakdown is a hint, not a diagnosis.** `lcpBreakdown` tells which phase (server response, discovery, download, render) is the biggest for image LCPs; it does not name the element or the script. The subparts are only returned for sites with enough image LCPs.
- **Not a lab test.** Field data cannot say which element or script is slow. Use it to see who is ahead and then investigate with a lab tool.

### Related actor

[Competitor Ad Monitor for Google Ads](https://apify.com/solidfetch/google-ads-transparency-scraper) shows every Google ad the same competitors run, and can return only the new ones each day.

### Attribution

CrUX datasets by Google are licensed under a Creative Commons Attribution 4.0 International License (CC BY 4.0). Every row carries this in its `source` field, and you should keep that attribution when you publish the data. The actor is not affiliated with Google.

When a run fails, an error report with the error message and the shape of the input (which fields are set, how long the lists are), never your input values, your API key or your results, is sent to the developer to fix bugs.

# Actor input Schema

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

Domains, origins or page URLs: your client and its competitors. A bare domain such as example.com becomes the origin https://example.com. Up to 200 per run.

## `level` (type: `string`):

Origin gives the data of all pages of a site together and is the one to compare competitors with. URL gives one page, which only has data when it gets enough visits; with Origin, a page URL is cut down to its site.

## `formFactor` (type: `string`):

Which devices the numbers describe. Google ranks on mobile data, so Phone is the usual choice for a comparison.

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

Adds the p75 of each metric for earlier weeks from the CrUX History API. Costs one more request per domain, not an extra charge.

## `historyPeriods` (type: `integer`):

How many collection periods the history covers, 1 to 40. Each period is a 28-day window that moves forward one week. Used when Include history is on.

## `apiKey` (type: `string`):

Optional. Leave empty to use the actor's own key. Paste your own free Chrome UX Report API key for long lists or frequent schedules: in the Google Cloud console enable Chrome UX Report API for a project, then create an API key under Credentials.

## Actor input object example

```json
{
  "urls": [
    "web.dev",
    "developer.chrome.com"
  ],
  "level": "origin",
  "formFactor": "ALL",
  "includeHistory": false,
  "historyPeriods": 25
}
```

# Actor output Schema

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

No description

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

No description

# 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": [
        "web.dev",
        "developer.chrome.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("solidfetch/crux-competitor-benchmark").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": [
        "web.dev",
        "developer.chrome.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("solidfetch/crux-competitor-benchmark").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": [
    "web.dev",
    "developer.chrome.com"
  ]
}' |
apify call solidfetch/crux-competitor-benchmark --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,solidfetch/crux-competitor-benchmark"
        }
    }
}
```

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/yxMgbdkhRcvk4qrR5/builds/hqhifPtJ1da7JRq9f/openapi.json
