# PageSpeed Lighthouse Audit - Bulk Core Web Vitals & SEO Score (`neverempty/pagespeed-lighthouse-audit`) Actor

For SEO agencies and site owners: Lighthouse 13 performance, accessibility, best-practices and SEO scores, Core Web Vitals (LCP, CLS, TBT) and top fixes per URL. No API key, no PageSpeed quota. Monitor reports only real moves (scores drift 10-16 points on their own). Unauditable pages are free.

- **URL**: https://apify.com/neverempty/pagespeed-lighthouse-audit.md
- **Developed by:** [NeverEmpty](https://apify.com/neverempty) (community)
- **Categories:** SEO tools, Developer tools, Automation
- **Stats:** 4 total users, 3 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $24.00 / 1,000 lighthouse audit returneds

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

## PageSpeed Lighthouse Audit - Bulk Core Web Vitals & SEO Score

Give it a list of URLs and get one clean row per page with the **Google Lighthouse** scores you know from PageSpeed Insights: **performance, accessibility, best practices and SEO (0-100)**, the **Core Web Vitals** lab metrics (**LCP, CLS, TBT**, with good / needs-improvement / poor ratings), and the **top fixes** with their estimated savings. Lighthouse 13 runs in its own Chrome inside the Actor, so there is **no API key and no PageSpeed Insights quota** to run out of.

Turn on **monitor mode** and schedule it: you only get the pages whose score **really** moved. Lighthouse performance scores move on their own between runs (in our runs the same page ranged 10 to 16 points), so a move is only reported when a second audit confirms it, and it has to be bigger than the range that page has shown before.

Pages this Actor cannot audit (robots.txt says no, sign-in pages, check pages, 404s, PDFs) come back as a **free row that says why**, and a run that cannot audit any page is **not charged at all**, not even the start fee.

### What you get per page

| Column | What it is |
|---|---|
| `performanceScore`, `accessibilityScore`, `bestPracticesScore`, `seoScore` | Lighthouse category scores, 0-100 (`agenticBrowsingScore` when you ask for Lighthouse 13's agentic-browsing category) |
| `largestContentfulPaintMs`, `cumulativeLayoutShift`, `totalBlockingTimeMs` | Core Web Vitals lab metrics. TBT is the lab stand-in for INP: Interaction to Next Paint needs a real user interaction, so a page-load audit cannot measure it (the same is true on PageSpeed Insights' lab section) |
| `lcpRating`, `clsRating`, `tbtRating`, `fcpRating` | `good` / `needs-improvement` / `poor` (LCP 2.5 s / 4 s, CLS 0.1 / 0.25, TBT 200 ms / 600 ms, FCP 1.8 s / 3 s) |
| `firstContentfulPaintMs`, `speedIndexMs`, `timeToInteractiveMs`, `serverResponseTimeMs` | Other Lighthouse timings |
| `totalByteWeight`, `requestCount`, `domElements`, `mainThreadWorkMs`, `javascriptExecutionMs` | Page weight and main-thread cost |
| `opportunities` | Performance improvements that did not pass (Lighthouse insights and diagnostics), biggest estimated saving first: `id`, `title`, `displayValue`, `score`, `savingsLcpMs`, `savingsFcpMs`, `savingsTbtMs`, `savingsCls`, `savingsMs`, `savingsBytes` |
| `failedAudits` | Accessibility, best-practices and SEO checks that failed: `category`, `id`, `title`, `displayValue`, `itemCount` |
| `strategy`, `formFactor`, `throttlingMethod`, `cpuSlowdownMultiplier` | How the page was audited (mobile or desktop, simulated throttling) |
| `benchmarkIndex`, `slowHostCpu` | Lighthouse's own CPU benchmark of the machine that ran the audit, and whether Lighthouse would call it slow (1,000 or less). Performance scores depend on CPU, so this is here for you to see, not hidden |
| `lighthouseVersion`, `browserVersion`, `runWarnings`, `auditedAt` | Lighthouse 13.5.0, Chrome version, Lighthouse's warnings (for example "The page loaded too slowly to finish within the time limit") |
| `url`, `finalUrl`, `redirects`, `httpStatus`, `lighthouseFinalUrl` | The URL you gave, where it ended after redirects |
| `changeType`, `changedScores`, `previousCheckedAt`, `confirmedByRuns`, `watchName` | Monitor mode (see below) |
| `status`, `note` | `ok` for an audit. Any other status is a free row, and `note` says why |

If Apify moves a long run to another server in the middle, pages already returned are not audited or charged again.

Scores and metrics that Lighthouse did not produce are `null`, never 0 or a guess. If Lighthouse could not score a category you asked for, the page is a free `audit-incomplete` row instead of a charged audit.

### Input

| Field | Default | What it does |
|---|---|---|
| `urls` | (example: github.com/apify/crawlee) | 1 to 500 URLs. Without `http(s)://`, `https://` is added. Duplicates are audited once |
| `strategy` | `mobile` | `mobile`, `desktop` or `both` (two rows per URL, each charged as one audit) |
| `categories` | performance, accessibility, best-practices, seo | Also `agentic-browsing`. Fewer categories do not change the price |
| `maxOpportunities` | 10 | How many performance fixes to list (0 to 30) |
| `pageLoadTimeoutSecs` | 45 | Lighthouse's maxWaitForLoad (15 to 90) |
| `onlyChanges` | false | Monitor mode: return only pages whose score moved |
| `watchName` | (empty) | Name of the watch list, so separate schedules do not mix |
| `performanceChangeThreshold` | 10 | Smallest move of the performance score, in points, that counts as a change |
| `changeThreshold` | 5 | Smallest move of the accessibility, best-practices, SEO (and agentic-browsing) scores that counts as a change |
| `resetMonitoringState` | false | Forget the remembered scores of this watch |

Example:

```json
{
  "urls": ["https://github.com/apify/crawlee", "https://developer.mozilla.org/en-US/"],
  "strategy": "both",
  "categories": ["performance", "seo"]
}
```

### Monitor mode: only real score changes

Schedule the Actor (for example daily) with `onlyChanges: true` and a `watchName`:

1. **First run**: every page is audited twice and returned once as `changeType: "first-check"` (the two audits give the page's usual score and how much it wobbles).
2. **Later runs**: each page is audited once and compared with its usual score (the median of up to 8 earlier audits). A category counts as moved only if it moved by at least its threshold (`performanceChangeThreshold`, default 10, for performance; `changeThreshold`, default 5, for the others) **and** by more than the range that page has already shown. Otherwise the page comes back as a free `no-change` row with its current scores (and the move that would have been needed), charged as one `url-checked` event.
3. If a category did move, the page is **audited again straight away**. Only when both audits moved the same way is it returned as `changeType: "changed"` with `changedScores`, for example `{"performance": {"previous": 57, "current": 36, "delta": -21, "firstAudit": 35, "requiredMove": 18}}`. If the second audit does not confirm it, it is treated as noise and not returned. If the second audit could not finish at all, the page comes back as a free `unconfirmed` row with the first audit's scores (never as "no change"), nothing is remembered, and the next run checks it again.

Why: in our own runs the same page scored 40 and then 26 a few minutes later, and another 29 and then 15; over 5 to 6 audits, python.org ranged 72 to 82, MDN 74 to 86 and a Wikipedia article 64 to 76. Selling those wobbles as "changes" would be noise you pay for.

### Pricing (pay per event)

- **Audit returned**: one row with Lighthouse scores for one URL on one device.
- **Run start**: once per run that returned at least one audit (in monitor mode: that finished at least one comparison).
- **URL checked** (monitor mode only): a page that was audited and compared but did not move. You get a free `no-change` row with its current scores.

Not charged: robots.txt refusals, sign-in pages, check pages, pages that do not exist or are not HTML, pages Lighthouse could not audit, input errors, and anything left when the run reaches your maximum charge or its timeout. A run whose maximum total charge has no room for the start fee plus one audit audits nothing and is charged nothing.

### Measured

Our own runs on Apify (September 2026, 4,096 MB, Lighthouse 13.5.0, mobile):

| Input | Time | Result |
|---|---|---|
| The example URL only (`{}`) | 63 s for the whole run | 1 audit (github.com/apify/crawlee: performance 32, accessibility 97, best practices 100, SEO 100) |
| 11 URLs of all kinds | 171 s | 5 audits; robots.txt refusal, sign-in redirect, 404, check page (HTTP 403), PDF and an unknown domain answered as 6 free rows without starting Chrome |
| 16 well-known sites, 5 categories | 740 s | 11 audits; 2 refused by the site (HTTP 403), 1 robots.txt refusal, 2 pages Lighthouse could not finish (never stopped loading) as free rows |

- One audit takes about 10 s (example.com) to 65 s (heavy pages), about 30 s on average, so roughly 60 to 120 pages fit in an hour.
- Peak memory 2.1 GB (16 pages in one run).
- Lighthouse's CPU benchmark of the machine (`benchmarkIndex`) was about 950 to 2,600 at 4,096 MB. At 2,048 MB it was 756 to 904 (below Lighthouse's own "slow machine" line of 1,000) and the same pages scored 20 to 30 points lower (python.org 49 and 59 against 79 and 72; MDN 57 and 66 against 83 and 86), so this Actor runs at 4,096 MB by default. Keep it there.
- Checked against a browser: for 10 audited pages (python.org, MDN, a Wikipedia article, example.com, gov.uk, bbc.com/news), the SEO and accessibility checks in `failedAudits` (meta description, html lang, document title, image alt text) and the final URL were compared with the same pages opened in Chromium: 49 of 50 agree. The one difference: on bbc.com/news the browser showed 8 grey lazy-loading image placeholders without alt text that Lighthouse's check did not count at the moment it ran.

### How it works

1. **Preflight** with a plain HTTP request (no browser): robots.txt of the site (RFC 9309, prefix match), redirects one hop at a time, sign-in redirects, check pages, 404s and non-HTML. These are answered in a few seconds without starting Chrome.
2. **Lighthouse 13** audits the page in headless Chrome with Lighthouse's default settings for the device (simulated throttling: Moto G Power, slow 4G, 4x CPU slowdown for mobile). Pages are audited **one at a time**, because Lighthouse's own guidance is not to run audits in parallel on one machine: they compete for CPU and the performance score drops.
3. A failed audit that can be temporary (Chrome crashed, no first paint, HTTP 5xx or 429) is retried once. A clear answer (404, DNS failure, not HTML) is not, and neither is a page that never stops loading (Lighthouse's PAGE\_HUNG, or an audit still running after the page load limit plus 90 seconds): those come back as a free `audit-failed` row instead of costing you twice the time.
4. The run stops starting new audits shortly before its timeout, so audits already done are delivered and charged, and the rest are listed in a free `time-limit` row.

### Limits

- **Lab data, not field data.** These are Lighthouse lab numbers, like the "Diagnose performance issues" part of PageSpeed Insights. The Chrome UX Report field data (real users' 28-day LCP / INP / CLS) is not included.
- **Scores can differ from PageSpeed Insights by a few points**, as they do between any two machines: Lighthouse's performance score depends on the CPU. Each row carries `benchmarkIndex` so you can see the machine's speed. Compare a page with itself over time (monitor mode) rather than with another tool.
- **robots.txt is respected.** A site whose robots.txt disallows the page (for example a staging site with `Disallow: /`) is not audited.
- No sign-in, no CAPTCHA solving, no proxies to get around a refusal.
- One audit takes about 10 to 65 seconds (roughly 60 to 120 pages per hour). Raise the run timeout for long lists.

### Support

Something wrong or missing? Open an issue on the Issues tab with the run ID and we will look at it.

# Actor input Schema

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

Page URLs to audit with Google Lighthouse, one per line (1 to 500 per run). A URL without http:// or https:// is read as https://. The same URL given twice (also when only the #fragment differs) is audited and charged once. Pages are audited one at a time (about 10 to 65 seconds each), so for long lists raise the run timeout. If this is empty, the example URL https://github.com/apify/crawlee is audited.

## `strategy` (type: `string`):

mobile = Lighthouse's default mobile run (Moto G Power screen, slow 4G and 4x CPU slowdown, simulated), the same settings PageSpeed Insights uses for its mobile tab. desktop = Lighthouse's desktop settings. both = two rows per URL (one mobile, one desktop), each charged as one audit.

## `categories` (type: `array`):

Which Lighthouse categories to score (0-100). performance includes the Core Web Vitals lab metrics (LCP, CLS, TBT) and the list of improvements; accessibility, best-practices and seo list the checks that failed. agentic-browsing is Lighthouse 13's newer category (llms.txt, agent accessibility tree, WebMCP). Asking for fewer categories does not change the price.

## `maxOpportunities` (type: `integer`):

How many performance improvements (Lighthouse insights and diagnostics that did not pass, largest estimated saving first) to include in the opportunities column. 0 leaves the column empty.

## `pageLoadTimeoutSecs` (type: `integer`):

Lighthouse's maxWaitForLoad: how long Lighthouse waits for the page to finish loading before it scores what it has (it then adds a run warning). 45 is Lighthouse's default.

## `onlyChanges` (type: `boolean`):

Compare each page with its usual score (the median of up to 8 earlier audits under the same watchName) and return only pages where a category score moved by its threshold or more, and by more than the range that page has shown before. A move is only reported when a second audit, run straight away, moved the same way too, so normal Lighthouse score noise is not reported as a change. The first run audits each page twice (to learn its usual score and range) and returns it once as first-check. Pages that did not move come back as free no-change rows and are charged as one url-checked event each (the Lighthouse run itself), not as an audit.

## `watchName` (type: `string`):

Optional name for this watch list (letters, digits, dot, dash, underscore; up to 40). Use a different name for each separate schedule so their remembered scores do not mix. When set without monitor mode, every page is returned with its changeType against the remembered scores.

## `performanceChangeThreshold` (type: `integer`):

In monitor mode, how many points (0-100 scale) the performance score has to move from its usual score to count as a change. The performance score moves on its own between runs: in our own runs the same page ranged 10 to 16 points over 5 to 6 audits (for example 64 to 76). 10 keeps most of that noise out; a page that has shown a wider range needs a correspondingly bigger move.

## `changeThreshold` (type: `integer`):

In monitor mode, how many points the accessibility, best-practices, SEO (and agentic-browsing) scores have to move to count as a change. These scores only move when the page itself changes (or a third-party script behaves differently), so a smaller threshold works.

## `resetMonitoringState` (type: `boolean`):

Start this watch from scratch: every page is returned again as first-check.

## Actor input object example

```json
{
  "urls": [
    "https://github.com/apify/crawlee",
    "https://developer.mozilla.org/en-US/"
  ],
  "strategy": "mobile",
  "categories": [
    "performance",
    "accessibility",
    "best-practices",
    "seo"
  ],
  "maxOpportunities": 10,
  "pageLoadTimeoutSecs": 45,
  "onlyChanges": false,
  "performanceChangeThreshold": 10,
  "changeThreshold": 5,
  "resetMonitoringState": false
}
```

# Actor output Schema

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

One row per URL and device: Lighthouse performance, accessibility, best-practices and SEO scores (0-100), Core Web Vitals lab metrics (LCP, CLS, TBT with good / needs-improvement / poor ratings, plus FCP, Speed Index, TTI, server response time), page weight and request count, the top performance improvements with their estimated savings, the failed accessibility, best-practices and SEO checks, and the Lighthouse version, throttling and CPU benchmark used. In monitor mode, changeType and changedScores. A URL that robots.txt does not allow, that needs a sign-in, that shows a check page, that does not exist or that Lighthouse could not audit comes back as a free row that says why.

# 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://github.com/apify/crawlee",
        "https://developer.mozilla.org/en-US/"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/pagespeed-lighthouse-audit").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://github.com/apify/crawlee",
        "https://developer.mozilla.org/en-US/",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("neverempty/pagespeed-lighthouse-audit").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://github.com/apify/crawlee",
    "https://developer.mozilla.org/en-US/"
  ]
}' |
apify call neverempty/pagespeed-lighthouse-audit --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,neverempty/pagespeed-lighthouse-audit"
        }
    }
}
```

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/cpiJeGnYsTlM4mQnb/builds/THF8uGV7K5NgoJBaD/openapi.json
