# Bulk PageSpeed Insights: Lighthouse & Sitemap Audits (`rovnaki/pagespeed-bulk`) Actor

Audit URL lists or sitemaps with Google PageSpeed Insights. Export mobile/desktop Lighthouse scores and opportunities. $0.005 per successful URL × device, plus start.

- **URL**: https://apify.com/rovnaki/pagespeed-bulk.md
- **Developed by:** [rovnaki](https://apify.com/rovnaki) (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 url analyzeds

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

## Bulk PageSpeed Insights: Lighthouse & Sitemap Audits

Audit a list of public URLs or expand a sitemap. Get Lighthouse category scores, lab metrics and optimization opportunities as JSON or CSV. Mobile and desktop each count as one analysis; failed analyses have no result fee. Supply your encrypted `psiApiKey` for larger runs, or use the creator-key fallback capped at 25 URL × strategy analyses per run.

Use bulk PageSpeed Insights as a Lighthouse API for a sitemap performance audit or mobile desktop speed test. Export LCP, CLS and TBT lab measurements through PageSpeed CSV export or JSON. Core Web Vitals field data is optional, when PSI supplies it; lab results are not a real-user Core Web Vitals assessment.

**$0.005 per successful delivered URL × strategy**, plus a start event of $0.00005 per GB (one-GB minimum). No browser or proxy runs inside this Actor.

### Quick start

```json
{
  "urls": [
    "https://example.com/"
  ],
  "strategy": "mobile",
  "categories": [
    "performance",
    "seo"
  ],
  "maxUrls": 1,
  "maxChargedResults": 1,
  "concurrency": 1
}
```

One successful result costs **$0.0052 with a 4 GB start**, or **$0.00505 at ≤1 GB**, before any separately applicable platform usage. Inspect **`OUTPUT`** in the default key-value store for per-analysis errors, counts, stop reasons and sitemap diagnostics.

Illustrative compact row (full results also include opportunities and optional field data):

```json
{"url":"https://example.com/","strategy":"mobile","scores":{"performance":83,"seo":92},"lab":{"lcp_ms":2400,"cls":0.05,"tbt_ms":120},"field":{"url":null,"origin":null}}
```

### Key setup

Optionally enable the PageSpeed Insights API in your Google Cloud project, create a key and enter it in the **PageSpeed Insights API key** (`psiApiKey`) secret input field. Apify encrypts this field when saving input; the Actor reads and decrypts it through `Actor.getInput()`. Your supplied key takes priority over the creator's environment secret. Use the secret input editor; do not save a real key in example JSON, source files or datasets.

If `psiApiKey` is omitted or blank, the Actor uses the creator's **`PSI_API_KEY`** environment secret and logs a clear fallback message. This mode starts at most **25 URL × strategy analyses per run**, including failed analyses. `both` counts twice per URL; the last URL may receive only one strategy when the cap is reached. Supply your own encrypted key for larger runs. User-key runs still obey URL, result and platform budget limits. The fallback is shared quota, not a guarantee of availability; it can return quota errors.

The cap counts analyses rather than retries: each analysis can make up to four HTTP attempts with retry/backoff. It applies across direct URLs and sitemap results and cannot be raised by increasing input limits. Remaining work is reported as `OUTPUT.status: limited` and `stopped_reason: creator-key-analysis-limit`. Failed analyses have no result fee but still consume the fallback allowance. The cap resets each run; it is not a per-user or daily quota.

If neither key is available, startup fails before sitemap expansion or upstream requests. Keys are never logged or included in results; PSI request URLs are not logged and generated errors redact the key. For local development, use a private local input or export `PSI_API_KEY`. Google quota and any project charges apply to the key's project; no fixed daily quota is promised.

### Sitemap example

The owner-controlled public sitemap below is capped at five URLs and two strategies: up to **10 results ($0.05 plus start)**, or $0.0502 at 4 GB / $0.05005 at ≤1 GB.

```json
{
  "sitemapUrl": "https://gearchecks.com/sitemap-index.xml",
  "strategy": "both",
  "categories": ["performance", "seo"],
  "maxUrls": 5,
  "maxSitemaps": 10,
  "maxChargedResults": 10,
  "concurrency": 1
}
```

You can supply both explicit URLs and a sitemap; explicit URLs take priority. URLs must be HTTP(S), without embedded credentials. Fragments are removed and duplicates are skipped before the URL cap and strategy expansion. Distinct query strings and paths are retained. Redirected canonical pages may still have distinct input rows.

### What you get

- Performance, accessibility, SEO and best-practices scores on a 0–100 scale.
- Lab LCP, CLS, TBT, FCP, Speed Index and TTI, if present in the selected Lighthouse version and categories.
- URL and origin CrUX p75 metrics, categories and distributions from PSI's `loadingExperience` and `originLoadingExperience` responses. LCP/INP use milliseconds; CLS is unitless. A Core Web Vitals assessment is included for each field-data scope.
- Top opportunities sorted by estimated time savings, then byte savings. Supports legacy Lighthouse opportunities and newer `metricSavings` audits. Savings are estimates and should not be added together.
- Sitemap indexes, nested sitemaps, namespaces, gzip, URL deduplication, configurable caps, retry/backoff and per-analysis error isolation.

Field data is optional and can disappear from PSI. Google plans to remove CrUX data from this API; no CrUX or INP availability is promised. The source is the [official PSI v5 API](https://developers.google.com/speed/docs/insights/v5/reference/pagespeedapi/runpagespeed), including its available CrUX data. The Actor does not make separate CrUX API calls.

### Inputs

| Input | Default | Description |
|---|---|---|
| `psiApiKey` | Unset | Optional encrypted personal PSI key; blank/omitted uses creator `PSI_API_KEY` with a 25-analysis cap, including failures |
| `urls` | `[]` | Up to 100,000 explicit URLs; supply this or `sitemapUrl` |
| `sitemapUrl` | Unset | XML sitemap or sitemap index |
| `strategy` | `mobile` | `mobile`, `desktop` or `both` |
| `strategies` | Unset | API alternative: array of `mobile`/`desktop`; omit `strategy` when using it |
| `categories` | All four | One or more of `performance`, `accessibility`, `seo`, `best-practices` |
| `maxUrls` | `100` | 1–100,000 unique URLs before multiplying by strategy |
| `maxSitemaps` | `25` | 1–1,000 sitemap documents; bounds the queue too |
| `concurrency` | `4` | 1–20 simultaneous PSI requests, including their retries |
| `timeoutSecs` | `120` | 10–300 seconds per fetch attempt |
| `maxChargedResults` | `200000` | Run-wide emitted URL × strategy cap; `0` avoids upstream work |
| `topOpportunities` | `10` | 0–100 opportunities per result |

Keep concurrency low enough for your key's quota. HTTP 429/5xx and transient network failures get up to three retries. Exponential backoff starts at 500 ms, honors `Retry-After` seconds/dates, and caps waits at 60 seconds. Permanent failures such as HTTP 400/403 are not retried. Persistent quota failures are recorded without charging a result.

Sitemap expansion is breadth-first, skips duplicate/cyclic index URLs and stops at the configured caps. Each fetched document and decompressed XML is limited to 10 MB. External entities/DTDs are rejected. A bad child sitemap is reported while other queued documents continue. It does not crawl page links or discover sitemaps from robots.txt.

### Output

One dataset item per successful unique URL × strategy. For example, 100 URLs with `both` produces up to 200 items. This abbreviated example shows the score/metric layout; full results also contain timestamps, field data, opportunities and warnings:

```json
{
  "url": "https://example.com/",
  "final_url": "https://example.com/",
  "strategy": "mobile",
  "scores": {
    "performance": 83,
    "accessibility": 100,
    "seo": 92,
    "best-practices": 96
  },
  "lab": {
    "lcp_ms": 2400,
    "cls": 0.05,
    "tbt_ms": 120,
    "fcp_ms": 800,
    "si_ms": 1800,
    "tti_ms": null
  },
  "field": {"url": null, "origin": null},
  "opportunities": [],
  "warnings": []
}
```

Each available field-data object includes `id`, `overall_category`, `core_web_vitals` and a `metrics` map keyed by Google's names, such as `LARGEST_CONTENTFUL_PAINT_MS`, `INTERACTION_TO_NEXT_PAINT` and `CUMULATIVE_LAYOUT_SHIFT_SCORE`. Each metric contains `p75`, `category`, `unit` and `distributions`. PSI represents CLS percentiles and bucket bounds as hundredths; these are converted to unitless values. The returned `id` preserves the source scope, including any origin fallback chosen by Google.

Core Web Vitals passes when all three p75 values are present and LCP ≤ 2,500 ms, INP ≤ 200 ms and CLS ≤ 0.1. It fails if any known metric exceeds its threshold; otherwise missing values yield `unknown`. Lab metrics do not substitute for field data.

Opportunities contain `id`, `title`, `description`, `savings_ms`, `savings_bytes` and `display_value`. Time savings use the largest time-metric estimate, excluding unitless CLS savings. Values without published estimates are `null`.

The **Scores and lab metrics** and **CrUX and opportunities** dataset views support quick inspection and export. Use JSON for nested data. The `OUTPUT` record in the default key-value store includes task counts, per-URL-strategy errors, budget stop reasons and sitemap expansion diagnostics/cap flags. Failed PSI calls and Lighthouse runtime errors produce no dataset row and no custom event. If every analysis fails, `OUTPUT.status` is `failed`; mixed results are `partial`; a cap that prevents remaining work is `limited`. Check this record when monitoring runs, since isolated upstream errors do not abort the SDK process.

### Pricing and budget behavior

The `url-strategy-analyzed` event costs **$0.005 per successful delivered item**. Two strategies cost $0.01 per URL. A missing CrUX section does not invalidate an otherwise successful Lighthouse analysis. Invalid input, failed analyses, sitemap requests and duplicates do not produce custom charges. The default start event is automatic and is not charged again in code.

Dataset writes and `Actor.charge()` calls are serialized. Each write is guarded by the platform charging manager, strict remaining dollar capacity and result limits. `maxChargedResults`, `ACTOR_MAX_TOTAL_CHARGE_USD`, `ACTOR_MAX_CHARGED_RESULTS`, `ACTOR_MAX_PAID_DATASET_ITEMS` and `APIFY_MAX_PAID_DATASET_ITEMS` are all honored, including zero and prior charges. Local runs apply configured caps too. Some in-flight requests may finish after the cap is reached; excess rows are not delivered or charged. Check the Store/Console for any separately applicable platform usage; this source does not establish platform-cost allocation. The local memory default remains 512 MB (maximum 1,024 MB), but remote default run memory must also be checked; a previously observed 4,096 MB default requires a separate metadata update. Existing tasks or explicit run overrides can select different memory.

Dataset storage and charging are not atomic. The Actor charges only after a successful write. If charging is rejected, that last row can remain in the dataset and the run fails immediately after workers settle, preventing further delivery.

### Limitations

- A valid supplied `psiApiKey` or creator `PSI_API_KEY` and available project quota are required. Creator-key mode is limited to 25 analyses per run. Concurrency is a ceiling, not a per-minute quota guarantee. Retries consume time and can still exhaust quota.
- [CrUX data needs sufficient real-user samples](https://developers.google.com/speed/docs/insights/v5/about); many pages or origins have none. Missing data stays `null`. Field data reflects Google's reporting window, not this run's synthetic measurement.
- Scores and audits vary by Lighthouse version, region and measurement conditions. Unrequested categories and unavailable lab metrics remain `null`. Newer Lighthouse versions may omit TTI.
- Estimated opportunities are not guaranteed savings. Unitless-only CLS opportunities are not included in the time/byte ranking.
- Sitemaps may omit pages. URL caps count unique input URLs, not successful analyses. Sitemap caps may omit later branches; expansion diagnostics report limits and errors.
- This version does not implement historical run diffs, authentication into target pages, full Lighthouse report storage, a separate CrUX API fallback or browser rendering in the Actor.

### Local development and publishing

```bash
source ~/.nvm/nvm.sh
nvm use 22
npm ci
npm run build
npm test
npm run smoke
```

Run commands inside this Actor directory. Tests use representative synthetic PSI and sitemap fixtures plus injected transports; no API key or live calls are needed. The smoke command builds and invokes the real SDK entrypoint using `INPUT.json` and isolated `.smoke-storage`, validates the dataset schema and prints one JSON summary. Without `PSI_API_KEY`, it emits an explicit `skipped: true` summary and exits successfully. With a key, failed live checks exit non-zero.

For local runs, put input at `storage/key_value_stores/default/INPUT.json`, export the key and use `npm start` after building, or use the Apify CLI. Node 22 and a multi-stage `apify/actor-node:22` Dockerfile match the reference Actor's stack. Only production dependencies go into the runtime image.

`.actor/actor.json` links the input schema, output links, dataset schemas/views and this Store listing. Before publishing a paid Actor, configure the prices in `.actor/pay_per_event.json` through Apify's pricing configuration; this deployment manifest does not activate pricing on its own. Disable synthetic `apify-default-dataset-item` charges to prevent double billing. The Actor fails closed for missing custom PPE configuration or an active synthetic dataset-item price. Production budget checks use the charging manager's event prices; local checks use the listed price.

# Actor input Schema

## `psiApiKey` (type: `string`):

Optional personal Google PSI API key, stored encrypted and read through the SDK. If omitted or blank, uses the creator PSI_API_KEY and caps this run at 25 URL × strategy analyses, including failures. Supply your key for larger runs. Retries can make up to four HTTP attempts per analysis.

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

HTTP(S) URLs; fragments removed and duplicates skipped. Combined with sitemap results, explicit URLs come first.

## `sitemapUrl` (type: `string`):

Optional XML sitemap or sitemap index; nested indexes and gzip supported. Supply URLs or a sitemap.

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

Mobile, desktop, or one analysis for each.

## `strategies` (type: `array`):

Optional array of mobile/desktop. Omit strategy when using this alternative. Allowed values: mobile, desktop.

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

Unrequested or unavailable scores and metrics are null. Allowed values: performance, accessibility, seo, best-practices.

## `maxUrls` (type: `integer`):

Cap unique explicit and sitemap URLs before multiplying by strategy.

## `maxSitemaps` (type: `integer`):

Bounds nested sitemap expansion and the child sitemap queue; each document limited to 10 MB.

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

Concurrent PSI analyses. Dataset writes and charges are serialized.

## `maxChargedResults` (type: `integer`):

Run-wide delivered row cap. Zero performs no upstream work. Also respects platform dollar and result limits.

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

Per fetch attempt. Slow analyses are retried on network failures, 429 and 5xx.

## `topOpportunities` (type: `integer`):

Return the largest estimated time or byte savings; zero disables this list.

## Actor input object example

```json
{
  "urls": [
    "https://example.com/"
  ],
  "strategy": "mobile",
  "categories": [
    "performance",
    "seo"
  ],
  "maxUrls": 1,
  "maxSitemaps": 25,
  "concurrency": 1,
  "maxChargedResults": 1,
  "timeoutSecs": 120,
  "topOpportunities": 10
}
```

# Actor output Schema

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

Normalized results; export as JSON, CSV or Excel.

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

Counts, errors, source expansion and budget stop reasons.

# 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://example.com/"
    ],
    "strategy": "mobile",
    "categories": [
        "performance",
        "seo"
    ],
    "maxUrls": 1,
    "concurrency": 1,
    "maxChargedResults": 1
};

// Run the Actor and wait for it to finish
const run = await client.actor("rovnaki/pagespeed-bulk").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://example.com/"],
    "strategy": "mobile",
    "categories": [
        "performance",
        "seo",
    ],
    "maxUrls": 1,
    "concurrency": 1,
    "maxChargedResults": 1,
}

# Run the Actor and wait for it to finish
run = client.actor("rovnaki/pagespeed-bulk").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://example.com/"
  ],
  "strategy": "mobile",
  "categories": [
    "performance",
    "seo"
  ],
  "maxUrls": 1,
  "concurrency": 1,
  "maxChargedResults": 1
}' |
apify call rovnaki/pagespeed-bulk --silent --output-dataset

```

## MCP server setup

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

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/aDjDUFLEjInnjIWPx/builds/kOM50B5WyWCMj7jFc/openapi.json
