# Bulk PageSpeed & Lighthouse Checker: Core Web Vitals from CSV (`nerolabs/bulk-pagespeed-checker`) Actor

Google PageSpeed Insights (Lighthouse) scores, Core Web Vitals and top speed fixes for every URL in an Apify dataset, CSV or Google Sheet, keeping every original column. Inputs: datasetId or fileUrl, strategy. Charged per completed report. Agent-ready: x402, MCP.

- **URL**: https://apify.com/nerolabs/bulk-pagespeed-checker.md
- **Developed by:** [Adam Pearce](https://apify.com/nerolabs) (community)
- **Categories:** SEO tools, Lead generation, Agents
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.50 / 1,000 page tests

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

## Bulk PageSpeed & Lighthouse Checker (Dataset, CSV or Sheet)

**How fast is every website on this list, does it pass Google's Core Web Vitals, and what would fix it?**

Pasting sites into PageSpeed Insights one at a time is a browser tab and a minute of waiting per page. This takes the list you already have, a dataset, a CSV, an Excel file or a Google Sheet, and adds Google's own PageSpeed Insights (Lighthouse) results to every row: the performance, accessibility, best practices and SEO scores, the speed timings, the real-visitor Core Web Vitals verdict, and the top fixes ranked by how much time they would save.

Every original column comes back untouched, so nothing has to be matched up by hand afterwards.

### Who uses this

- **Agencies and freelancers finding prospects.** Run a scraped list of local businesses, keep only the sites scoring under 50, and every row already says what is wrong ("Reduce unused JavaScript, Est savings of 1,520 KiB"). That is the opening line of the pitch.
- **Client reporting.** Test every client site each week with a schedule and append to one named dataset, so you have a speed history per client without opening PageSpeed once.
- **SEO audits of a whole site.** Feed it a list of page URLs (paths are kept) and see which templates are slow, which fail Core Web Vitals and which have SEO checks failing.
- **Checking before and after a change.** Run the same list before and after a redesign, a new host or a plugin clean-up, then compare with [Diff & Change Detector](https://apify.com/nerolabs/dataset-diff-detector).

### Where the data comes from

From **Google PageSpeed Insights**, Google's own official API, which runs Lighthouse on Google's servers. This is the same test as pagespeed.web.dev, not an imitation of it. No scraping and no browser on our side. The Actor includes its own Google API key, so you do not need one.

There are two kinds of result, and both come back:

- **Lab data**: Google loads the page once on a simulated phone (or computer) and times it. This gives the 0 to 100 scores and timings such as LCP (how long until the main content shows). Lab scores move a few points between runs of the same page; that is normal Lighthouse variation, not an error.
- **Field data**: how the page actually performed for real Chrome visitors over the last 28 days, from Google's Chrome UX Report. This is what the **Core Web Vitals pass or fail** verdict is based on, and it is what Google uses in search ranking. It only exists for pages or sites with enough traffic; for a small site the field columns are empty rather than estimated, and `FieldDataScope` says whether the data is for the exact page or the whole site.

### What you get on every row

Columns are prefixed with the device (`mobile`, and `desktop` too if you test both), so the two sit side by side:

| Column | What it is |
|---|---|
| `mobilePerformanceScore` | 0 to 100. Google's bands: 90+ good, 50 to 89 needs improvement, under 50 poor. `mobilePerformanceRating` says which |
| `mobileAccessibilityScore`, `mobileBestPracticesScore`, `mobileSeoScore` | The other three Lighthouse scores, 0 to 100 |
| `mobileLcpSeconds`, `mobileFcpSeconds`, `mobileSpeedIndexSeconds`, `mobileTbtMs`, `mobileCls` | Lab timings: main content, first content, visual speed, blocking time, layout shift |
| `mobileServerResponseMs`, `mobilePageWeightKb` | How long the server took to answer, and how heavy the page is |
| `mobileCoreWebVitals` | `pass` or `fail` for real visitors (LCP, INP and CLS all good), empty when Google has no field data |
| `mobileFailingVitals` | Which vitals failed and by how much, e.g. `CLS 0.13 (needs improvement)` |
| `mobileFieldLcpMs`, `mobileFieldInpMs`, `mobileFieldCls` | The real-visitor 75th percentile values |
| `mobileTopFixes` | Google's suggested fixes, biggest estimated saving first |
| `mobileFailedSeoChecks`, `mobileFailedAccessibilityChecks` | The pass/fail checks the page failed, e.g. `Document does not have a meta description` |
| `testedUrl`, `finalUrl`, `redirected` | What was tested and where it ended up. A redirect, even just to `www.`, costs load time and is flagged |
| `pageSpeedStatus`, `mobileStatusDetail` | `ok`, or why there is no report (see below) |
| `pageSpeedReportUrl` | A link to open the same test on pagespeed.web.dev |

The same page listed twice (`bbc.co.uk` and `https://bbc.co.uk/`) is tested and charged once, and both rows get the result, the second marked `isDuplicate`.

### Example

Real output for python.org on mobile, from a run on 23 September 2026:

```json
{
    "company": "Python",
    "website": "https://www.python.org/",
    "testedUrl": "https://www.python.org/",
    "pageSpeedStatus": "ok",
    "redirected": false,
    "mobilePerformanceScore": 77,
    "mobilePerformanceRating": "needs_improvement",
    "mobileAccessibilityScore": 75,
    "mobileBestPracticesScore": 96,
    "mobileSeoScore": 92,
    "mobileLcpSeconds": 4.98,
    "mobileCoreWebVitals": "pass",
    "mobileFieldDataScope": "page",
    "mobileFieldLcpMs": 1211,
    "mobileTopFixes": "Render-blocking requests (Est savings of 2,510 ms); Reduce unused CSS (Est savings of 128 KiB); Reduce unused JavaScript (Est savings of 59 KiB)",
    "mobileFailedSeoChecks": "Links do not have descriptive text"
}
```

Notice the page passes Core Web Vitals for real visitors while scoring 77 in the lab. That is common, and it is why both are returned: the lab score shows what to fix, the field verdict shows what Google ranks on.

The run summary (`PAGESPEED_SUMMARY` in the key-value store) adds the average score per category, the ten slowest pages, and the fixes that come up most often across the whole list, which is usually the fastest way to see what a set of sites has in common.

### Input

- **Dataset**, **File URL** (CSV, TSV, Excel, JSON, JSON Lines, or a Google Sheet shared as "anyone with the link") or **inline data**.
- **URL field**: the column with the website. Detected automatically in most lists. Bare domains, full URLs and paths all work.
- **Device**: mobile (what Google ranks on, the default), desktop, or both.
- **Scores to include**, **Fixes to list per page**, **Rows to keep** (all, slow only, fast only, failing Core Web Vitals only, problems only) and the **slow threshold** (default 50).
- **Google API key**: optional, only if you want your own daily allowance for very large lists.
- **Exports**: CSV and/or Excel files, a named dataset to append to, and a webhook for the run summary.

### What it costs

**$0.005 per page tested per device**, which is $5.00 per 1,000 tests at the standard rate and less on Bronze, Silver and Gold. Testing both mobile and desktop is two tests. Files are $0.01 each, a webhook delivery is $0.02.

**You are charged for a report, not for an attempt.** A page Google could not load (the site is down, blocks Google, or never paints), a value that is not a web address, a rate limit and a timeout are all **free**, and each row says why.

Filtering happens after testing, so keeping only the slow rows does not make the run cheaper.

A 1,000-site list on mobile costs about **$5**. Google takes 10 to 90 seconds per test and the Actor runs 8 at a time, so 1,000 sites take roughly one to two hours; set it off and come back, or schedule it overnight.

### FAQ

**Is this allowed?** Yes. PageSpeed Insights is Google's public API, provided for exactly this. Nothing is scraped.

**Why is my score a few points different from pagespeed.web.dev?** Every Lighthouse run is a fresh page load, so lab scores move a little between runs, on Google's own site too. For a decision, look at the rating band and the field verdict rather than a two-point difference.

**Why are the Core Web Vitals columns empty?** Google only publishes real-visitor data for pages and sites with enough Chrome traffic. Most small business sites have none, and this Actor will not invent it. The lab scores are still there.

**Why did a site come back `page_failed`?** Google's own servers could not load it: the domain does not resolve, the server refused, or the site blocks Google. The reason is in `mobileStatusDetail`, and it was not charged. That is useful on its own for a prospect list.

**I am seeing `rate_limited`.** Lower Concurrency, or add your own free Google API key for your own daily allowance.

**Can I feed last week's output back in?** Yes, that is how a weekly check works. Any PageSpeed column already in your rows is replaced with this run's result (and the run tells you it did), so an old score is never left looking current.

**Do I need a Google API key?** No. Add one only if you run many thousands of pages a day.

### The rest of the toolkit

The natural pair is **[Tech Stack Detector](https://apify.com/nerolabs/tech-stack-detector)**, **[Domain WHOIS & Age Checker](https://apify.com/nerolabs/domain-whois-checker)** and **[Website Contact Finder](https://apify.com/nerolabs/website-contact-finder)**: run them on the same list, or chain them with the Pipeline Runner, to get every company's site speed, tech stack, domain age and business inbox in one table.

For the data itself: [Dataset Cleaner & Exporter](https://apify.com/nerolabs/dataset-cleaner-exporter), [Filter & Transform](https://apify.com/nerolabs/dataset-filter-transform), [Join & Merge](https://apify.com/nerolabs/dataset-join-merge), [Aggregate, Group By & Pivot](https://apify.com/nerolabs/dataset-aggregate-pivot), [Diff & Change Detector](https://apify.com/nerolabs/dataset-diff-detector), [AI Enrich](https://apify.com/nerolabs/dataset-ai-enrich), [Charts & Report](https://apify.com/nerolabs/dataset-charts-report), [to Postgres, Supabase & MySQL](https://apify.com/nerolabs/dataset-to-database), [to REST API](https://apify.com/nerolabs/dataset-to-rest-api), and [Actor Pipeline Runner](https://apify.com/nerolabs/actor-pipeline-runner) to chain them in one call.

***

If this saved you pasting a few hundred sites into PageSpeed Insights one at a time, a review on the Store page helps a lot. If something looks wrong, open an issue on the Issues tab and I will answer personally.

# Actor input Schema

## `datasetId` (type: `string`):

An Apify dataset whose rows each hold a website or page URL. Use the picker so the run is allowed to read it. Leave empty to use a file URL or inline data instead.

## `fileUrl` (type: `string`):

A public link to a CSV, TSV, Excel (.xlsx), JSON or JSON Lines file, or a Google Sheet shared as 'anyone with the link'. Used when no dataset is set.

## `data` (type: `array`):

Rows as a JSON array, each with a website or URL field. Used when neither a dataset nor a file URL is set.

## `fileFormat` (type: `string`):

How to read the file URL. 'Detect automatically' works from the extension, the content type and the first bytes.

## `urlField` (type: `string`):

The column holding each website or page, for example 'website' or 'url'. Leave empty to detect it automatically. Bare domains, full URLs with paths and even an email address all work: 'bbc.co.uk' is tested as 'https://bbc.co.uk/', and a path such as '/pricing' is kept, so you can audit specific pages.

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

Google tests the page as a mid-range phone on a slow connection, or as a computer. Google ranks on the mobile version, so most people want mobile. 'Both' runs two tests per page and adds mobile\* and desktop\* columns side by side.

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

Which Lighthouse scores to return. The price is the same whichever you pick; leaving some out makes each test a little faster.

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

How many of Google's 'what would make this faster' suggestions to return per page, biggest estimated time saving first (for example 'Reduce unused JavaScript (Est savings of 588 KiB)'). 0 turns them off.

## `keep` (type: `string`):

Which rows end up in the results. Filtering happens after testing, so it does not change what a run costs.

## `slowBelowScore` (type: `integer`):

Sets the 'Slow sites only' and 'Fast sites only' filters and the slow count in the summary. Google's own bands: 90 and up is good, 50 to 89 needs improvement, under 50 is poor.

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

Optional. The Actor includes its own key, so you do not need one. Add your own free Google PageSpeed Insights API key only if you run very large lists and want your own daily allowance (25,000 tests a day). Stored encrypted and never shown in the results.

## `concurrency` (type: `integer`):

How many pages Google tests at once. Each test takes Google 10 to 90 seconds, so this is what makes big lists quick. Lower it if you see 'rate\_limited' rows.

## `requestTimeoutSecs` (type: `integer`):

How long to wait for Google to finish one test. Heavy pages can genuinely take over a minute. A test that times out is retried once and is never charged.

## `maxItems` (type: `integer`):

A safety cap on how many rows are read from the input. Leave empty for no cap (up to 10,000).

## `exportFormats` (type: `array`):

Optionally write the results as a real downloadable CSV and/or Excel file as well as the dataset.

## `outputDatasetName` (type: `string`):

Optional. The name of a dataset to append every run's results to, so a scheduled weekly speed check builds one growing history instead of a new dataset each time.

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

Optional. When the run finishes, the summary (average scores, slowest pages, most common fixes, download links) is POSTed here as JSON, so a scheduled speed watch can report into Slack, Zapier, Make, n8n or your own API.

## Actor input object example

```json
{
  "data": [
    {
      "company": "Example Domain",
      "website": "example.com"
    },
    {
      "company": "Python",
      "website": "https://www.python.org/"
    },
    {
      "company": "Wikipedia",
      "website": "www.wikipedia.org"
    }
  ],
  "fileFormat": "auto",
  "strategy": "mobile",
  "categories": [
    "performance",
    "accessibility",
    "best-practices",
    "seo"
  ],
  "maxOpportunities": 5,
  "keep": "all",
  "slowBelowScore": 50,
  "concurrency": 8,
  "requestTimeoutSecs": 150
}
```

# Actor output Schema

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

Every original row with its Lighthouse scores, lab timings, Core Web Vitals and top fixes added.

## `pageSpeedSummary` (type: `string`):

Average scores, slowest pages, most common fixes and the data note.

# 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 = {
    "data": [
        {
            "company": "Example Domain",
            "website": "example.com"
        },
        {
            "company": "Python",
            "website": "https://www.python.org/"
        },
        {
            "company": "Wikipedia",
            "website": "www.wikipedia.org"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("nerolabs/bulk-pagespeed-checker").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 = { "data": [
        {
            "company": "Example Domain",
            "website": "example.com",
        },
        {
            "company": "Python",
            "website": "https://www.python.org/",
        },
        {
            "company": "Wikipedia",
            "website": "www.wikipedia.org",
        },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("nerolabs/bulk-pagespeed-checker").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 '{
  "data": [
    {
      "company": "Example Domain",
      "website": "example.com"
    },
    {
      "company": "Python",
      "website": "https://www.python.org/"
    },
    {
      "company": "Wikipedia",
      "website": "www.wikipedia.org"
    }
  ]
}' |
apify call nerolabs/bulk-pagespeed-checker --silent --output-dataset

```

## MCP server setup

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

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/JA83jsthUR6Va1WZl/builds/E4DQd4v4F7eI10eoT/openapi.json
