CLI tool for batch Google PageSpeed Insights analysis with CSV/JSON/HTML reports
Project description
PageSpeed Insights Batch Analysis Tool
A command-line tool that automates Google PageSpeed Insights analysis across multiple URLs, extracting performance metrics (lab + field data) into structured CSV, JSON, and HTML reports.
Installation
Run instantly with uvx (recommended, no install needed)
uvx pagespeed quick-check https://example.com
Install with pip or pipx
pip install pagespeed
pagespeed quick-check https://example.com
Run from URL (just needs uv)
uv run https://raw.githubusercontent.com/volkanunsal/pagespeed/main/pagespeed_insights_tool.py quick-check https://example.com
Development
git clone https://github.com/volkanunsal/pagespeed.git
cd pagespeed
uv run pagespeed_insights_tool.py quick-check https://example.com
Prerequisites
- Python 3.13+
- Google API key (optional) — without one, you're limited to ~25 queries/day; with one, ~25,000/day
Getting an API Key
- Go to the Google Cloud Console
- Create a new project (or select an existing one)
- Navigate to APIs & Services > Library
- Search for PageSpeed Insights API and enable it
- Go to APIs & Services > Credentials
- Click Create Credentials > API Key
- Copy the key and set it:
export PAGESPEED_API_KEY=your_key_here
Or add it to yourpagespeed.tomlconfig file (see Configuration).
Usage
quick-check — Fast single-URL spot check
Prints a formatted report to the terminal. No files written.
# Mobile only (default)
pagespeed quick-check https://www.google.com
# Both mobile and desktop
pagespeed quick-check https://www.google.com --device both
# With specific categories
pagespeed quick-check https://www.google.com --categories performance accessibility
Sample output:
============================================================
URL: https://www.google.com
Strategy: mobile
============================================================
Performance Score: 92/100 (GOOD)
--- Lab Data ---
First Contentful Paint............. 1200ms
Largest Contentful Paint........... 1800ms
Cumulative Layout Shift............ 0.0100
Speed Index........................ 1500ms
Total Blocking Time................ 150ms
Time to Interactive................ 2100ms
audit — Full batch analysis
Analyzes multiple URLs and writes CSV/JSON reports.
# From a file of URLs
pagespeed audit -f urls.txt
# Multiple strategies and output formats
pagespeed audit -f urls.txt --device both --output-format both
# Inline URLs with custom output path
pagespeed audit https://a.com https://b.com -o report
# With a named profile
pagespeed audit -f urls.txt --profile full
# Piped input
cat urls.txt | pagespeed audit
# Include full Lighthouse audit data in JSON output
pagespeed audit -f urls.txt --full --output-format json
# Stream results as NDJSON to stdout as they complete
pagespeed audit -f urls.txt --stream
# Pipe streamed results into jq for real-time filtering
pagespeed audit -f urls.txt --stream | jq '.performance_score'
# Stream and filter to only failing URLs
pagespeed audit -f urls.txt --stream | jq 'select(.performance_score < 50)'
--full flag
Pass --full to embed the complete raw lighthouseResult object from the PageSpeed API into each result in the JSON output. This includes all Lighthouse audits, opportunities, diagnostics, and metadata — useful for deep analysis or feeding into other tools.
- JSON: each result gains a top-level
lighthouseResultkey containing the full API object. - CSV:
--fullis silently ignored; the raw object is never written to CSV. - File naming: auto-named files get a
-fullsuffix (e.g.,20260219T143022Z-mobile-full.json).
--stream flag
Pass --stream to print results to stdout as NDJSON (one JSON object per line) as each URL/strategy completes, instead of buffering everything and writing files at the end. This lets you pipe results into jq, grep, or other tools without waiting for the full batch to finish.
- Output: one
json.dumpsline per result written to stdout immediately on completion. - File output: skipped — no CSV/JSON files are written in stream mode.
- Summary: the post-run audit summary table is suppressed (not useful when piping).
- Progress bar: still shown on stderr so you can track progress while piping stdout.
- Budget: still evaluated if
--budgetis set, using the complete result set.
# Stream all results to stdout
pagespeed audit -f urls.txt --stream
# Extract a single field from each result
pagespeed audit -f urls.txt --stream | jq '.performance_score'
# Filter to only URLs below a score threshold
pagespeed audit -f urls.txt --stream | jq 'select(.performance_score < 50)'
# Save streamed results to a file while also viewing them
pagespeed audit -f urls.txt --stream | tee results.ndjson | jq '.url'
Each NDJSON line is a flat JSON object with the same fields as a CSV row (url, strategy, performance_score, lab_fcp_ms, etc.). null is used where a value is not available.
The URL file is one URL per line. Lines starting with # are comments:
# Main pages
https://example.com
https://example.com/about
https://example.com/contact
compare — Compare two reports
Loads two previous report files and shows per-URL score changes.
# Compare before and after
pagespeed compare before.csv after.csv
# Custom threshold (flag changes >= 10%)
pagespeed compare --threshold 10 old.json new.json
Output flags regressions with !! and improvements with ++.
report — Generate HTML dashboard
Creates a self-contained HTML report from a results file.
# Generate HTML from CSV results
pagespeed report results.csv
# Custom output path
pagespeed report results.json -o dashboard.html
# Auto-open in browser
pagespeed report results.csv --open
The HTML report includes:
- Summary cards (total URLs, average/best/worst scores)
- Color-coded score table (green/orange/red)
- Core Web Vitals pass/fail indicators
- Bar charts comparing scores across URLs
- Field data table (when available)
- Sortable columns (click headers)
run — Low-level direct access
Full control with every CLI flag. Same internals as audit.
pagespeed run https://example.com --device desktop --categories performance accessibility --delay 2.0
pipeline — End-to-end analysis
Resolves URLs from a sitemap (or file/inline), runs the analysis, writes CSV/JSON data files, and generates an HTML report — all in one command. Optionally evaluates a performance budget.
# From a sitemap (auto-detected from URL shape)
pagespeed pipeline https://example.com/sitemap.xml
# Limit URLs and auto-open report in browser
pagespeed pipeline https://example.com/sitemap.xml --sitemap-limit 20 --open
# Filter to a section of the sitemap, both devices
pagespeed pipeline https://example.com/sitemap.xml --sitemap-filter "/blog/" --device both
# Inline URLs
pagespeed pipeline https://a.com https://b.com --device both
# From a URL file
pagespeed pipeline -f urls.txt --open
# Data files only — skip HTML report generation
pagespeed pipeline -f urls.txt --no-report --output-format json
# Evaluate Core Web Vitals budget (exits 2 on failure)
pagespeed pipeline https://example.com/sitemap.xml --budget cwv
# Custom budget with GitHub Actions output format
pagespeed pipeline https://example.com/sitemap.xml --budget budget.toml --budget-format github
Sitemap auto-detection: when a single positional argument looks like a sitemap (ends in .xml, contains sitemap in the path, or the file content starts with <?xml), it is treated as a sitemap source automatically. Pass --sitemap explicitly to use a sitemap alongside inline URLs.
pipeline flags
| Flag | Short | Default | Description |
|---|---|---|---|
source |
— | [] |
Sitemap URL/path (auto-detected) or plain URLs |
--file |
-f |
None | File with one URL per line |
--sitemap |
— | None | Explicit sitemap URL or local path |
--sitemap-limit |
— | None | Max URLs to extract from sitemap |
--sitemap-filter |
— | None | Regex to filter sitemap URLs |
--open |
— | False |
Auto-open HTML report in browser after completion |
--no-report |
— | False |
Skip HTML report; write data files only |
--budget |
— | None | Budget file (TOML) or cwv preset — exits 2 on failure |
--budget-format |
— | text |
Budget output format: text, json, or github |
--webhook |
— | None | Webhook URL for budget result notifications |
--webhook-on |
— | always |
When to send webhook: always or fail |
All audit flags (--device, --output-format, --output, --output-dir, --delay, --workers, --categories) also apply.
Configuration
Config file: pagespeed.toml
An optional TOML file for persistent settings and named profiles. The tool searches for it in:
- Current working directory (
./pagespeed.toml) - User config directory (
~/.config/pagespeed/config.toml)
You can also pass an explicit path with --config path/to/config.toml.
[settings]
api_key = "YOUR_API_KEY" # or use PAGESPEED_API_KEY env var
urls_file = "urls.txt" # default URL file for -f
delay = 1.5 # seconds between API requests
device = "mobile" # mobile, desktop, or both
output_format = "csv" # csv, json, or both
output_dir = "./reports" # directory for output files
workers = 4 # concurrent workers (1 = sequential)
categories = ["performance"] # Lighthouse categories
verbose = false
[profiles.quick]
device = "mobile"
output_format = "csv"
categories = ["performance"]
[profiles.full]
device = "both"
output_format = "both"
categories = ["performance", "accessibility", "best-practices", "seo"]
[profiles.core-vitals]
device = "both"
output_format = "csv"
categories = ["performance"]
[profiles.client-report]
urls_file = "client_urls.txt"
device = "both"
output_format = "both"
output_dir = "./client-reports"
categories = ["performance", "accessibility", "seo"]
Config resolution order
Settings are merged with the following priority (highest wins):
- CLI flags — explicit command-line arguments
- Profile values — via
--profile name [settings]— defaults from config file- Built-in defaults — hardcoded in the script
Global flags
| Flag | Short | Default | Description |
|---|---|---|---|
--api-key |
— | config/env | Google API key |
--config |
-c |
auto-discovered | Path to config TOML |
--profile |
-p |
None | Named profile from config |
--verbose |
-v |
False | Verbose output to stderr |
--version |
— | — | Print version and exit |
audit / run flags
| Flag | Short | Default | Description |
|---|---|---|---|
urls |
— | [] |
Positional URLs |
--file |
-f |
None | File with one URL per line |
--device |
— | mobile |
mobile, desktop, or both |
--output-format |
— | csv |
csv, json, or both |
--output |
-o |
auto-timestamped | Explicit output file path |
--output-dir |
— | ./reports/ |
Directory for auto-named files |
--delay |
-d |
1.5 |
Seconds between requests |
--workers |
-w |
4 |
Concurrent workers |
--categories |
— | performance |
Lighthouse categories |
--full |
— | False |
Embed raw lighthouseResult in JSON output (ignored for CSV) |
--stream |
— | False |
Print results as NDJSON to stdout as they complete (skips file output) |
Output Formats
File naming
By default, output files use UTC timestamps:
{output_dir}/{YYYYMMDD}T{HHMMSS}Z-{strategy}.{ext}
Examples:
./reports/20260216T143022Z-mobile.csv
./reports/20260216T150000Z-both.json
./reports/20260216T143022Z-report.html
Use -o to override with an explicit path.
CSV
Flat table with one row per (URL, strategy) pair. Columns:
| Column | Description |
|---|---|
url |
The analyzed URL |
strategy |
mobile or desktop |
performance_score |
0-100 Lighthouse score |
lab_fcp_ms |
First Contentful Paint (ms) |
lab_lcp_ms |
Largest Contentful Paint (ms) |
lab_cls |
Cumulative Layout Shift |
lab_speed_index_ms |
Speed Index (ms) |
lab_tbt_ms |
Total Blocking Time (ms) |
lab_tti_ms |
Time to Interactive (ms) |
field_* |
Field (CrUX) metrics (when available) |
error |
Error message if the request failed |
JSON
Structured with metadata header:
{
"metadata": {
"generated_at": "2026-02-16T14:30:22+00:00",
"total_urls": 5,
"strategies": ["mobile", "desktop"],
"tool_version": "1.0.0"
},
"results": [
{
"url": "https://example.com",
"strategy": "mobile",
"performance_score": 92,
"lab_metrics": { "lab_fcp_ms": 1200, "lab_lcp_ms": 1800, ... },
"field_metrics": { "field_lcp_ms": 2100, "field_lcp_category": "FAST", ... },
"error": null
}
]
}
With --full, each result also includes the complete raw lighthouseResult from the API:
{
"results": [
{
"url": "https://example.com",
"strategy": "mobile",
"performance_score": 92,
"lab_metrics": { ... },
"field_metrics": { ... },
"lighthouseResult": {
"audits": { ... },
"categories": { ... },
"categoryGroups": { ... },
"configSettings": { ... },
"environment": { ... },
"fetchTime": "...",
"finalUrl": "https://example.com",
"lighthouseVersion": "...",
"requestedUrl": "https://example.com",
"runWarnings": [],
"stackPacks": [],
"timing": { ... },
"i18n": { ... }
},
"error": null
}
]
}
Metrics Reference
Lab data (synthetic, from Lighthouse)
| Metric | Good | Needs Work | Poor |
|---|---|---|---|
| First Contentful Paint | < 1.8s | 1.8s–3.0s | > 3.0s |
| Largest Contentful Paint | < 2.5s | 2.5s–4.0s | > 4.0s |
| Cumulative Layout Shift | < 0.1 | 0.1–0.25 | > 0.25 |
| Total Blocking Time | < 200ms | 200ms–600ms | > 600ms |
| Speed Index | < 3.4s | 3.4s–5.8s | > 5.8s |
| Time to Interactive | < 3.8s | 3.8s–7.3s | > 7.3s |
Field data (real users, from CrUX)
Field data comes from the Chrome User Experience Report. It may not be available for low-traffic sites.
| Metric | Description |
|---|---|
| FCP | First Contentful Paint — when first content appears |
| LCP | Largest Contentful Paint — when main content loads |
| CLS | Cumulative Layout Shift — visual stability |
| INP | Interaction to Next Paint — input responsiveness |
| FID | First Input Delay — (deprecated, replaced by INP) |
| TTFB | Time to First Byte — server response time |
Rate Limits
| Scenario | Limit |
|---|---|
| Without API key | ~25 queries/100 seconds |
| With API key | ~25,000 queries/day (400/100 seconds) |
Tips:
- Use
--delayto increase time between requests if hitting rate limits - The tool retries on 429 (rate limit) responses with exponential backoff
- See Concurrency Model for how
--workersand--delayinteract
Concurrency Model
The tool uses asyncio + httpx for non-blocking HTTP I/O.
How it works:
- With
--workers 1(or effectively 1), requests run strictly sequentially — one finishes before the next starts. - With
--workers N > 1(default: 4), all tasks are launched together viaasyncio.gather(). A sharedasyncio.Semaphore(1)ensures requests start no more than once per--delayseconds:- Each coroutine acquires the semaphore
- Sleeps the remainder of
delaysince the last request started - Records the timestamp and releases the semaphore
- Makes the actual HTTP request — outside the semaphore
Because the HTTP call happens after releasing the semaphore, multiple requests can be in-flight simultaneously even though they start delay seconds apart. Wall time is therefore much shorter than n_urls × (delay + latency); it converges toward n_urls × delay + avg_latency as the number of URLs grows.
Practical rule of thumb:
| Goal | Setting |
|---|---|
| Safest for rate limits | --workers 1 (sequential) |
| Default (balanced) | --workers 4 --delay 1.5 |
| Maximum throughput | --workers 4 --delay 1.0 (watch for 429s) |
Cron usage
Output files auto-increment with timestamps, so cron jobs won't overwrite previous results:
# Every Monday at 6am UTC
0 6 * * 1 cd /path/to/project && pagespeed audit -f urls.txt --profile full
Examples
The examples/ folder contains ready-to-use configuration files for common workflows:
| Example | Description |
|---|---|
basic/ |
Minimal config with API key, strategy, and a sample URL list |
multi-profile/ |
Named profiles for quick, full, and client-report workflows |
ci-budget/ |
Strict and lenient performance budgets for CI pipelines |
sitemap-pipeline/ |
Sitemap auto-discovery with regex filters and section-specific profiles |
Copy any example folder into your project and edit to taste. See examples/README.md for full details.
Testing
The project includes a comprehensive test suite (169 tests across 30 test classes). All tests run offline — API calls, sitemap fetches, and file I/O are mocked.
# Run all tests
uv run test_pagespeed_insights_tool.py -v
# Run a single test class
uv run test_pagespeed_insights_tool.py -v TestValidateUrl
# Run a specific test method
uv run test_pagespeed_insights_tool.py -v TestExtractMetrics.test_full_extraction
License
This project is licensed under the MIT License.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pagespeed-2.1.0.tar.gz.
File metadata
- Download URL: pagespeed-2.1.0.tar.gz
- Upload date:
- Size: 28.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ea373f209f9ea0144d34155724411e9d10bab672c45faf436afdae0f3820d3b2
|
|
| MD5 |
b37660527c4ff2dc2a613ccc02de678a
|
|
| BLAKE2b-256 |
bd04a54c283727457950e4ca9d1d3152bdd2fbec0f733a933293ae01040a2905
|
Provenance
The following attestation bundles were made for pagespeed-2.1.0.tar.gz:
Publisher:
publish.yml on volkanunsal/pagespeed
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pagespeed-2.1.0.tar.gz -
Subject digest:
ea373f209f9ea0144d34155724411e9d10bab672c45faf436afdae0f3820d3b2 - Sigstore transparency entry: 974545013
- Sigstore integration time:
-
Permalink:
volkanunsal/pagespeed@86b997385051f1ad21d8ca390dbdca1adcbcea4a -
Branch / Tag:
refs/tags/v2.1.0 - Owner: https://github.com/volkanunsal
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@86b997385051f1ad21d8ca390dbdca1adcbcea4a -
Trigger Event:
push
-
Statement type:
File details
Details for the file pagespeed-2.1.0-py3-none-any.whl.
File metadata
- Download URL: pagespeed-2.1.0-py3-none-any.whl
- Upload date:
- Size: 28.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
95dea5aefae08d80f3b5e1b5a995eb8751294bf892f4a9cb136683ab52010f8d
|
|
| MD5 |
ee5a29d08846d07f02bd33533f2bcc45
|
|
| BLAKE2b-256 |
ba9999101f397038862304b42aca955d11acb7afb2c1f75fbd9be0e7889d7263
|
Provenance
The following attestation bundles were made for pagespeed-2.1.0-py3-none-any.whl:
Publisher:
publish.yml on volkanunsal/pagespeed
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pagespeed-2.1.0-py3-none-any.whl -
Subject digest:
95dea5aefae08d80f3b5e1b5a995eb8751294bf892f4a9cb136683ab52010f8d - Sigstore transparency entry: 974545044
- Sigstore integration time:
-
Permalink:
volkanunsal/pagespeed@86b997385051f1ad21d8ca390dbdca1adcbcea4a -
Branch / Tag:
refs/tags/v2.1.0 - Owner: https://github.com/volkanunsal
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@86b997385051f1ad21d8ca390dbdca1adcbcea4a -
Trigger Event:
push
-
Statement type: