# Lighthouse & PageSpeed Audit: Bulk Core Web Vitals (`fernandosord/lighthouse-page-speed-monitor`) Actor

Run Google's open-source Lighthouse in Chrome on many URLs, without a PageSpeed API key. Get performance, accessibility, best-practices and SEO scores, LCP, CLS and TBT, top fixes, and alerts to Slack, Discord or a webhook when a score drops.

- **URL**: https://apify.com/fernandosord/lighthouse-page-speed-monitor.md
- **Developed by:** [Plainsource](https://apify.com/fernandosord) (community)
- **Categories:** SEO tools, Developer tools
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $25.00 / 1,000 page auditeds

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

### What this Actor does

Lighthouse Page Speed Monitor runs Google's open-source [Lighthouse](https://github.com/GoogleChrome/lighthouse) (version 13.5.0) in Chrome for a list of URLs and returns one row per URL and device with:

- Performance, accessibility, best-practices and SEO scores (0 to 100)
- Lab metrics: Largest Contentful Paint (LCP), Cumulative Layout Shift (CLS), Total Blocking Time (TBT), First Contentful Paint (FCP), Speed Index and Time to Interactive (TTI)
- The top performance opportunities with their estimated savings
- The accessibility and SEO audits that failed
- The final URL after redirects and the Lighthouse version
- Optionally, a link to the full HTML or JSON Lighthouse report

It does not use the PageSpeed Insights API, so there is no Google API key to set up and no daily quota. Lighthouse runs inside the Actor.

It also works as a monitor. Every run saves the scores per URL and device, and the next run compares against them. When a score drops by more than the points you set, or LCP gets slower by more than the percentage you set, the row is marked `regressed` and the Actor can post a message to Slack, Discord or any webhook.

### How it works

1. Each URL is checked first: it must be a public http or https address, its robots.txt must allow it, and every redirect is checked the same way.
2. Lighthouse loads the page in a fresh Chrome with the mobile or desktop settings that PageSpeed Insights also uses: an emulated Moto G Power on slow 4G for mobile, a 1350×940 screen on a fast connection for desktop, with simulated throttling.
3. URLs are audited one at a time, so audits do not compete for the CPU.
4. **First run:** the row status is `baseline` and the scores are saved.
5. **Later runs:** the row status is `ok` or `regressed`, with `scoreChanges` (points up or down per category), `lcpChangePercent` and the previous scores.

Scores are saved in a key-value store named `plainsource-lighthouse-page-speed-monitor-state` in your Apify account, grouped by **Watch list name**. Use a different watch list name for each task so separate lists keep separate history. The last 30 audits per URL and device are kept there.

### Accuracy: lab scores vary

Lighthouse lab scores move between runs even when nothing on the page changed. Network timing, server response time, ads and A/B tests all play a part. A performance score that moves 3 to 5 points between runs is normal; on pages with many scripts it can move more.

To get steadier numbers:

- Set **Lighthouse runs per URL** to 2 or 3. The row then reports the median run, chosen the way Lighthouse recommends (the run closest to the median FCP and TTI), and lists every run's performance score in `runPerformanceScores`.
- Keep the alert threshold at 10 points or more for performance.
- Compare runs of this Actor with each other. Scores also depend on the machine: on pages with heavy JavaScript, CPU speed changes TBT and the performance score.

What we measured in our tests (mobile, performance score):

| Page | Lighthouse CLI on a fast laptop (2 runs) | This Actor on Apify, 8 GB | This Actor on Apify, 4 GB |
|---|---|---|---|
| nodejs.org/en | 87, 88 | 75 | 54 |
| bbc.com/news | 59, 69 | 28 | 13 |
| wikipedia.org | 100, 100 | not run | 92, 100 |

Apify assigns CPU in proportion to memory, so the 8 GB default gives Chrome about two CPU cores, the minimum Lighthouse's own documentation recommends. With 4 GB (about one core) script-heavy pages score clearly lower, because the browser and Lighthouse share the core. Accessibility and SEO scores were the same as in the laptop runs for the pages we compared. Best practices can differ when a page loads different ads or third-party scripts per visit (bbc.com/news: 54 on the laptop, 77 on Apify).

### Input

The only required field is **URLs to audit**. All settings:

| Field | Default | What it does |
|---|---|---|
| **URLs to audit** (`urls`) | none | Full page URLs. Duplicates are removed. Up to 1,000 per run. |
| **Device** (`formFactor`) | `mobile` | `mobile`, `desktop`, or `both` (two audits per URL). |
| **Categories** (`categories`) | all four | Any of `performance`, `accessibility`, `best-practices`, `seo`. |
| **Lighthouse runs per URL** (`runsPerUrl`) | `1` | 1 to 3. With more than one, the median run is reported. Each run is charged. Applies only when `performance` is in the categories; otherwise one run is made. |
| **Opportunities to list** (`maxOpportunities`) | `5` | 0 to 20 performance opportunities per row, biggest saving first. |
| **Save the full report** (`saveReport`) | `none` | `none`, `html`, `json` or `both`. The report is saved in the run's key-value store and linked from the row. |
| **Watch list name** (`stateKey`) | `default` | Scores are saved and compared per watch list. Renaming starts a fresh baseline. |
| **Alert when a score drops by** (`scoreDropThreshold`) | `10` | Points. Any category that drops this much marks the row `regressed`. |
| **Alert when LCP rises by** (`lcpIncreasePercent`) | `25` | Percent. LCP must also be at least 100 ms slower. |
| **Webhook URL** (`webhookUrl`) | empty | An https URL that gets a POST when at least one page regressed. |
| **Webhook format** (`webhookFormat`) | `json` | `json` (the regressed rows without the opportunity and failed-audit lists), `slack` or `discord` (a readable message). |
| **Also notify about pages that failed** (`notifyOnErrors`) | `false` | Also send the message when pages could not be audited, and list them. |
| **Respect robots.txt** (`respectRobotsTxt`) | `true` | Skips pages the site's robots.txt disallows. Turn off only for sites you own or may test. |
| **Timeout per Lighthouse run** (`timeoutSecs`) | `90` | 60 to 300 seconds. A run that takes longer is reported as failed and not charged. |

Example input that audits three pages on mobile and desktop, three runs each, and posts regressions to Slack:

```json
{
    "urls": ["https://www.example.org/", "https://www.example.org/pricing", "https://www.example.org/blog/"],
    "formFactor": "both",
    "runsPerUrl": 3,
    "saveReport": "html",
    "stateKey": "example-site",
    "scoreDropThreshold": 10,
    "lcpIncreasePercent": 25,
    "webhookUrl": "https://hooks.slack.com/services/XXX/YYY/ZZZ",
    "webhookFormat": "slack"
}
```

### Output

One row per URL and device in the dataset. This is a real row from a test run on Apify (8 GB, first audit of this URL):

```json
{
    "url": "https://nodejs.org/en",
    "formFactor": "mobile",
    "status": "baseline",
    "performance": 75,
    "accessibility": 100,
    "bestPractices": 100,
    "seo": 100,
    "lcpMs": 3655,
    "cls": 0.029,
    "tbtMs": 568,
    "fcpMs": 1255,
    "speedIndexMs": 1255,
    "ttiMs": 3655,
    "regressions": [],
    "scoreChanges": null,
    "lcpChangePercent": null,
    "previousScores": null,
    "previousLcpMs": null,
    "previousAuditedAt": null,
    "opportunities": [
        {
            "id": "unused-javascript",
            "title": "Reduce unused JavaScript",
            "displayValue": "Est savings of 250 KiB",
            "lcpSavingsMs": 1200,
            "savingsKiB": 250
        },
        {
            "id": "render-blocking-insight",
            "title": "Render-blocking requests",
            "displayValue": "Est savings of 140 ms",
            "lcpSavingsMs": 150,
            "fcpSavingsMs": 150
        },
        {
            "id": "legacy-javascript-insight",
            "title": "Legacy JavaScript",
            "displayValue": "Est savings of 14 KiB"
        }
    ],
    "failedAccessibilityAudits": [],
    "failedSeoAudits": [],
    "finalUrl": "https://nodejs.org/en",
    "lighthouseVersion": "13.5.0",
    "runs": 1,
    "runPerformanceScores": [75],
    "reportUrl": null,
    "reportJsonUrl": null,
    "blockedRequests": 0,
    "benchmarkIndex": 3274,
    "warnings": [],
    "auditedAt": "2026-10-26T20:24:56.084Z",
    "error": null
}
```

On a later run, a regressed row has the reasons in `regressions`, for example `"Accessibility dropped 6 points (100 to 94)"` and `"LCP rose 140% (0.60 s to 1.44 s)"`, and `scoreChanges` such as `{"performance": -1, "accessibility": -6, "bestPractices": 0, "seo": 0}`.

Field notes:

- `failedAccessibilityAudits` and `failedSeoAudits` list up to 15 failed audits each, as `{ id, title, elements }`, where `elements` is the number of page elements or items the audit flagged.
- Opportunity savings are Lighthouse's own estimates: `lcpSavingsMs`, `fcpSavingsMs`, `tbtSavingsMs`, `clsSavings` and `savingsKiB` appear when Lighthouse gives them.
- `benchmarkIndex` is Lighthouse's CPU benchmark for the machine that ran the audit. Lighthouse warns when it is below 1000.
- `blockedRequests` counts requests the page tried to make to private or local addresses, which the Actor stops (see below).
- `warnings` holds Lighthouse's own run warnings and notes from the Actor.

`status` is one of:

- `baseline`: first audit under this watch list, scores saved
- `ok`: compared with the previous run, nothing dropped past your thresholds
- `regressed`: see `regressions`
- `failed`, `site_blocked`, `not_allowed`, `skipped`: the reason is in `error`

The run also writes a summary to the `OUTPUT` record of its key-value store, with counts per status and the list of regressed pages.

### Pricing

You pay per event, not for compute time:

| Event | Price |
|---|---|
| Run start | $0.002 per GB of run memory, so $0.016 per run at the default 8 GB |
| Page audited | $0.025 per Lighthouse run of one URL on one device |

`both` devices count as two audits per URL, and **Lighthouse runs per URL** multiplies the count: 10 URLs on mobile and desktop with 3 runs each are 60 audits. Not charged: pages that failed, timed out, were blocked, or were not allowed by robots.txt. Webhook messages and saved reports have no event price.

Example: 20 URLs on mobile, one run each, once a day, costs about 30 × (0.016 + 20 × 0.025) = $15.48 a month.

If a run reaches the maximum cost you set, the remaining URLs are marked `skipped` and their saved scores are kept.

### Scheduling and alerts

- **Schedule:** create a task with your URLs, then add it to a schedule in Apify Console (Schedules, then Create new). Each run compares with the previous one.
- **Slack:** create an incoming webhook in Slack, paste its URL into **Webhook URL**, and set the format to **Slack message**.
- **Discord:** in the channel settings, open Integrations, then Webhooks, then New Webhook, and copy the URL. Set the format to **Discord message**.
- **Other apps:** send JSON to Zapier, Make or n8n, or use the task's Integrations tab in Apify Console.

The webhook is called only when at least one page regressed (or, with **Also notify about pages that failed**, when a page failed). Page-derived text is escaped, so a page cannot ping a whole Slack or Discord channel.

### Limits and good manners

- **One URL at a time.** Lighthouse needs the CPU to itself, so audits run one after another. In our tests most audits took 8 to 30 seconds; very heavy pages can take a minute or reach the timeout.
- **Memory:** 8 GB by default (two CPU cores). 4 GB works and costs less per minute, but script-heavy pages score lower (see the accuracy table), and our 4 GB test run peaked at 3.4 GB of memory.
- **Robots.txt is respected by default.** If a site sets a `Crawl-delay`, audits of that site are spaced by it. Chrome's user agent ends with `PlainsourceLighthouseMonitor/1.0 (+https://apify.com/fernandosord/lighthouse-page-speed-monitor)`.
- **No proxies, logins or bot-check workarounds.** Pages that require a login or block automated visitors are reported as `site_blocked` and are not charged. LinkedIn, Facebook, Instagram, X and TikTok pages are not audited, because their terms forbid automated access.
- **Private networks are refused.** URLs on private or local addresses (such as `localhost`, `10.x.x.x`, `[::ffff:7f00:1]` or names that resolve to them) are refused, including through redirects. While Lighthouse runs, all of Chrome's traffic goes through a filter inside the Actor that resolves every host itself and stops requests to private and local addresses, so a page cannot reach them through a JavaScript redirect, fetch, XHR, iframe, image, websocket or worker either.
- **Pages only.** PDFs, images and other files are reported as `failed`; Lighthouse audits HTML pages.
- **Lab data only.** This Actor does not return Chrome UX Report field data (real-user metrics), which comes from Google's API.

### FAQ

**Why is my score different from PageSpeed Insights?** Both run Lighthouse with the same mobile and desktop settings, but on different machines and from different locations, and each run varies. Use 3 runs per URL for steadier numbers and compare this Actor's runs with each other. The accuracy section above shows what we measured.

**Why does the performance score change when I changed nothing?** That is normal lab variance (see above). Raise **Alert when a score drops by**, or use 2 or 3 runs per URL so the median smooths it out.

**Can I see the full Lighthouse report?** Set **Save the full report** to `html`. Each row gets a `reportUrl` with the same report Chrome DevTools shows.

**I got `site_blocked`. Can you get around it?** No. The site refused automated visitors, and this Actor respects that. Audit pages you own, or ask the site owner to allow the user agent above.

**How do I start over?** Change the **Watch list name**. The next run saves a new baseline.

**Can I call it from code or an AI agent?** Yes. Use the Apify API or the Apify MCP server with the same input. For example, with the API client: `client.actor("fernandosord/lighthouse-page-speed-monitor").call({ urls: [...] })`.

Lighthouse is developed by Google and published under the Apache 2.0 licence. This Actor is not made by or affiliated with Google.

Found a bug or need an option? Open an issue on this Actor's Issues tab.

# Actor input Schema

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

Full URLs of the pages to test, for example your home page, a product page and a blog post. Each URL is audited with Lighthouse in Chrome on the device you pick below. Redirects are followed and reported.

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

<b>Mobile</b> emulates a mid-range phone on a slow 4G connection, as PageSpeed Insights does. <b>Desktop</b> emulates a desktop on a fast connection. <b>Both</b> audits every URL twice and is charged twice.

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

Lighthouse categories to score. Leave empty for all four. Performance takes most of the time; the others add a few seconds.

## `runsPerUrl` (type: `integer`):

Lab scores vary from run to run, often by a few points of performance. With 2 or 3 runs the row reports the median run (the method Lighthouse recommends) and lists every run's performance score. Each run is charged. Applies only when the performance category is scored; otherwise one run is made.

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

How many performance opportunities (with estimated savings) to include per row, biggest first. 0 leaves them out.

## `saveReport` (type: `string`):

Save the complete Lighthouse report of each audit to the run's key-value store and add its link to the row. The HTML report is the same one Lighthouse and Chrome DevTools show. Reports are kept as long as the run's storage.

## `stateKey` (type: `string`):

Scores are saved per watch list, URL and device, and every run is compared with the previous one. Use a different name for each task, so separate lists don't share history. Renaming starts a fresh baseline.

## `scoreDropThreshold` (type: `integer`):

A row is marked <code>regressed</code> when any category score is this many points lower than in the previous run. Performance often moves 3 to 5 points between runs on its own, so values below 5 cause false alarms.

## `lcpIncreasePercent` (type: `integer`):

A row is also marked <code>regressed</code> when Largest Contentful Paint is this much slower than in the previous run (and at least 100 ms slower).

## `webhookUrl` (type: `string`):

Optional. When at least one page regressed, the run sends a POST to this https URL: a Slack or Discord incoming-webhook URL, or any endpoint that accepts JSON (Zapier, Make, n8n, your own server). Nothing is sent when nothing regressed.

## `webhookFormat` (type: `string`):

<b>JSON</b> sends the regressed rows (scores, metrics, regressions and report links, without the opportunity and failed-audit lists). <b>Slack</b> and <b>Discord</b> send a readable message with what dropped and the new scores.

## `notifyOnErrors` (type: `boolean`):

Include pages that could not be audited (not found, blocked, timed out) in the webhook message.

## `respectRobotsTxt` (type: `boolean`):

Skip pages that the site's robots.txt asks automated visitors not to open. Turn off only for sites you own or have permission to test.

## `timeoutSecs` (type: `integer`):

A Lighthouse run that takes longer is stopped and reported as failed (not charged). Lighthouse itself waits at most 45 s for the page to load; the rest is analysis.

## Actor input object example

```json
{
  "urls": [
    "https://www.wikipedia.org/"
  ],
  "formFactor": "mobile",
  "categories": [
    "performance",
    "accessibility",
    "best-practices",
    "seo"
  ],
  "runsPerUrl": 1,
  "maxOpportunities": 5,
  "saveReport": "none",
  "stateKey": "default",
  "scoreDropThreshold": 10,
  "lcpIncreasePercent": 25,
  "webhookFormat": "json",
  "notifyOnErrors": false,
  "respectRobotsTxt": true,
  "timeoutSecs": 90
}
```

# Actor output Schema

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

One row per URL and device: scores, lab metrics, top opportunities, failed accessibility and SEO audits, and any regression since the previous run.

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

Counts per status and the list of pages that regressed.

## `reports` (type: `string`):

Lighthouse HTML and JSON reports, when "Save the full report" is on. Each row links to its own report.

# 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.wikipedia.org/"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("fernandosord/lighthouse-page-speed-monitor").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.wikipedia.org/"] }

# Run the Actor and wait for it to finish
run = client.actor("fernandosord/lighthouse-page-speed-monitor").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.wikipedia.org/"
  ]
}' |
apify call fernandosord/lighthouse-page-speed-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,fernandosord/lighthouse-page-speed-monitor"
        }
    }
}
```

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/YEfbJcST1fbP9O9qr/builds/jYvPHcQJZOs2FFrVJ/openapi.json
