Run Google Core Web Vitals and PageSpeed audits against URLs. Use when asked to check site performance, CWV scores, LCP/CLS/INP/FCP/TTFB metrics, PageSpeed Insights, compare two sites, compare Shopify preview themes vs production, or bulk-audit URLs from a Google Sheet. Supports single URL, compare (two URLs side-by-side including Shopify theme QA), batch (multiple URLs), local measurement (Puppeteer, no API needed), and Google Sheet modes with CrUX field data preferred, lab fallback, and browser scraping for errors.
Audit website performance using Google's CrUX field data (real user metrics) and PageSpeed Insights API.
GOOGLE_PAGESPEED_API_TOKEN env var (Google PageSpeed Insights API key)gog CLI for Google Sheets operations (alternative to service account auth)agent-browser CLI for web.dev scraping fallbackThe scripts auto-load .env files from the working directory or skill root. No manual export needed.
If GOOGLE_PAGESPEED_API_TOKEN is in a .env file, the scripts will find it automatically. Users can also pass --api-key inline.
⚠️ NEVER echo, print, or display the API key value.
User provides one URL. Run the API, return formatted results inline.
User provides two URLs to compare. Run both, show side-by-side with winner highlighted per metric.
User provides URLs separated by line breaks. Run each, return formatted table.
Measure CWV locally using Puppeteer + web-vitals library. No API key needed. Supports desktop and mobile (with throttling). Use for batch measurements without API quota limits, testing auth-protected sites, or measuring Shopify preview URLs.
User provides a Sheet URL. Read URLs from column A, write results to columns B-N with conditional formatting.
Important: CrUX field data ≠ Lighthouse lab data. Field data reflects real users; lab data simulates a throttled mobile device. Always prefer field data — it's what Google uses for ranking signals.
Available in both API mode (CrUX field data) and local mode (lab measurement):
| Metric | Field (CrUX) | Lab (Local) | Good | Needs Improvement | Poor |
|---|---|---|---|---|---|
| LCP (s) | ✅ p75 | ✅ | ≤ 2.5 | 2.5–4.0 | > 4.0 |
| CLS | ✅ p75 | ✅ | ≤ 0.1 | 0.1–0.25 | > 0.25 |
| FCP (s) | ✅ p75 | ✅ | ≤ 1.8 | 1.8–3.0 | > 3.0 |
| TTFB (s) | ✅ p75 | ✅ | ≤ 0.8 | 0.8–1.8 | > 1.8 |
| Metric | Field (CrUX) | Lab (Local) | Good | Needs Improvement | Poor |
|---|---|---|---|---|---|
| INP (ms) | ✅ p75 | ❌ Not available | ≤ 200 | 200–500 | > 500 |
Why is INP not available in local mode?
INP requires PerformanceEventTiming entries from real user interactions. Puppeteer's synthetic events (click, keyboard, pointer) do not generate these browser API entries. This is a fundamental limitation of headless testing, not a bug.
Even Google Lighthouse doesn't measure INP in lab tests — it uses TBT as a proxy instead. Local mode follows the same pattern.
Calculated from synthetic testing, not available from CrUX field data:
| Metric | Description | Good | Needs Improvement | Poor |
|---|---|---|---|---|
| TBT (ms) | Total Blocking Time — time main thread blocked by long tasks (>50ms) between FCP and TTI | ≤ 200 | 200–600 | > 600 |
| SI (ms) | Speed Index — how quickly page contents are visually populated | ≤ 3400 | 3400–5800 | > 5800 |
| TTI (s) | Time to Interactive — when page becomes fully interactive (5s quiet window) | ≤ 3.8 | 3.8–7.3 | > 7.3 |
CWV Assessment: FAST / AVERAGE / SLOW (from CrUX overall_category, API mode only)
All handled by scripts/pagespeed-single.py. The script auto-detects the mode based on URL count:
# Single URL (Google API)
python3 scripts/pagespeed-single.py example.com
# Compare two URLs (Google API)
python3 scripts/pagespeed-single.py site-a.com site-b.com
# Batch (3+ URLs, Google API)
python3 scripts/pagespeed-single.py site1.com site2.com site3.com
# Local measurement (Puppeteer, no API)
python3 scripts/pagespeed-single.py --local example.com
# Local with mobile emulation
python3 scripts/pagespeed-single.py --local --mobile example.com
# Local compare mode
python3 scripts/pagespeed-single.py --local site-a.com site-b.com
# With inline API key
python3 scripts/pagespeed-single.py --api-key YOUR_KEY example.com
When --local is passed, the script shells out to scripts/pagespeed-local.js which uses Puppeteer + web-vitals library to measure CWV locally.
Prerequisites:
npm install puppeteer web-vitals in the skill directoryWhat it does:
evaluateOnNewDocument before page loadMobile mode (--mobile flag):
Output format:
📱 Mobile (Local (Puppeteer)):
CWV: LCP: 4.3s 🔴 | CLS: 0.31 🔴 | FCP: 1.6s 🟢 | TTFB: 0.1s 🟢
Lab: TBT: 450ms 🟡 | SI: 2880ms 🟢 | TTI: 5.2s 🟡
Data source labeled as "Local (Puppeteer)" vs "CrUX field" or "Lab"
When to use:
Limitations:
When comparing two URLs, detect if they share the same domain with ?preview_theme_id= parameters. If so, this is a Shopify theme QA comparison (not a competitor comparison). Adjust the output framing:
For theme QA, use this framing:
The script auto-loads .env from the working directory (searching upward). Run from the project root in a single Bash call:
cd /path/to/project && python3 /path/to/skills/core-web-vitals/scripts/pagespeed-single.py example.com
Or if the user's .env is elsewhere, source it first in the same command:
export $(grep -v '^#' /path/to/.env | xargs) && python3 scripts/pagespeed-single.py example.com
Do NOT:
Use scripts/pagespeed-bulk.py in this skill directory.
GOOGLE_PAGESPEED_API_TOKEN env var (or pass --api-key)--credentials) or gog CLI (--account)/d/ and /edit)# With service account
python3 -u scripts/pagespeed-bulk.py SPREADSHEET_ID --credentials service-account.json
# With gog CLI
python3 -u scripts/pagespeed-bulk.py SPREADSHEET_ID --account EMAIL
# Resume from index N, custom workers
python3 -u scripts/pagespeed-bulk.py SPREADSHEET_ID --credentials sa.json --start N --workers 6
tail -f /tmp/pagespeed.log
After main run completes, retry ERROR rows via web.dev:
python3 -u scripts/pagespeed-retry-browser.py SPREADSHEET_ID --credentials sa.json
Apply via Google Sheets batchUpdate API (see references/conditional-formatting.md). Colors:
Applied to all metric columns for all data rows.
spreadsheets/{id}/values/{range}?valueInputOption=RAW for whole-row writes (much faster than per-cell gog CLI calls).