Unlighthouse: Site-Wide Lighthouse Audits for Every Page
A practical unlighthouse guide for site-wide Lighthouse audits, CI budgets, crawling, route sampling, static reports, and reliable QA workflows.
Unlighthouse: Site-Wide Lighthouse Audits for Every Page
unlighthouse is the practical answer when one Lighthouse report is no longer enough. It discovers URLs across a site, runs Lighthouse for many pages, streams results into a dashboard, and can run in CI through unlighthouse-ci with score budgets. For QA engineers, that means performance, accessibility, SEO, and best-practices coverage can move from one hand-picked URL to a representative site-wide scan.
Use Unlighthouse when your risk lives in templates and route classes: product pages, docs pages, blog posts, localized content, logged-out marketing pages, and launch-critical funnels. It is not a replacement for a synthetic monitoring platform or real-user monitoring. It is a bulk audit runner for catching page-level regressions before they ship, especially when AI coding agents are editing layout, images, client bundles, metadata, or accessibility attributes across many routes.
The current Unlighthouse package is 0.18.0, and the official docs list Node.js 22.18+ as a requirement. The main commands to know are npx unlighthouse --site https://example.com for interactive scans and npx unlighthouse-ci --site https://example.com --budget 80 for CI. This guide focuses on crawler setup, route sampling, unlighthouse.config.ts, budgets, device and throttling choices, report hosting, and failure diagnosis. If you need single-page budget mechanics, pair it with the Lighthouse CI budgets guide. If accessibility is the main risk, keep the Lighthouse CI accessibility guide nearby.
Start with the scan boundary, not the score
The first Unlighthouse decision is not whether performance should be 80 or 90. It is what the scan is allowed to discover. The official URL discovery guide says Unlighthouse can use the provided site, explicit URLs, robots.txt, sitemap.xml, crawling internal links, and static route definitions. That is powerful, but it also means a poorly scoped scan can waste time on duplicate pages, admin paths, PDFs, search result pages, or routes that require authentication.
Think of the scan as a QA contract. A marketing pre-release scan might include homepage, pricing, comparison pages, integration pages, docs landing pages, and representative blog articles. A commerce scan might include category, product, cart, and checkout entry pages, but exclude account pages unless you configure authentication. A docs scan might include all versioned docs for a release branch but skip autogenerated API pages that are covered by a separate job.
| Scan question | Unlighthouse control | QA decision |
|---|---|---|
| Which host is tested? | site or --site | Prefer deployed preview URLs for release gates |
| Which URLs are eligible? | urls, scanner.include, scanner.exclude | Start explicit, expand after noise is understood |
| Should robots.txt apply? | scanner.robotsTxt | Keep enabled for public scans, disable only for intentional private checks |
| Should sitemap.xml apply? | scanner.sitemap | Use manual sitemap paths for multi-sitemap sites |
| Should HTML links be crawled? | scanner.crawler | Disable for huge sites once sitemap coverage is reliable |
| How many similar dynamic routes? | scanner.dynamicSampling | Sample templates, do not audit 5,000 near-identical pages |
The "what people get wrong" insight: teams argue about budgets before proving URL discovery is sane. A perfect budget on the wrong route set creates false confidence. First make the route inventory explainable. Then tighten thresholds.
Install and run the first audit
For one-off use, the official docs show npx unlighthouse --site https://mysite.com, with equivalent pnpm dlx and yarn dlx forms. A browser window opens with a dashboard where results stream in. For frequent use, install globally or add the package as a dev dependency. The package includes the unlighthouse and unlighthouse-ci binaries.
# One-time interactive scan.
npx unlighthouse --site https://example.com
# Package-manager alternatives.
pnpm dlx unlighthouse --site https://example.com
yarn dlx unlighthouse --site https://example.com
# Frequent local use.
npm install --save-dev unlighthouse
npx unlighthouse --site https://example.com --debug
If the first scan appears idle, add --debug. If Chrome is missing, the docs say Unlighthouse uses system Chrome and can download Chromium automatically. If you run in a minimal CI image, make browser dependencies part of the image or setup step rather than letting every job rediscover them.
| Environment | Recommended command | Notes |
|---|---|---|
| Developer laptop | npx unlighthouse --site https://preview.example.com | Use the dashboard for triage |
| Pull request CI | npx unlighthouse-ci --site https://preview.example.com --budget 80 | Fail on score regression below policy |
| Nightly full scan | npx unlighthouse-ci --config-file unlighthouse.nightly.config.ts --build-static | Generate a shareable report |
| Large site sampling | Config file with dynamicSampling and includes | Avoid scanning endless near-duplicates |
Do not run the first scan against production if the site has fragile analytics, bot controls, rate limits, or personalization. Use a preview or staging host where repeated Lighthouse traffic is acceptable.
Create a config file QA engineers can review
Unlighthouse looks for unlighthouse.config.ts, unlighthouse.config.js, or unlighthouse.config.mjs, and the docs use defineUnlighthouseConfig from unlighthouse/config. The import is optional according to the configuration guide, but it gives TypeScript users better editor feedback.
import { defineUnlighthouseConfig } from 'unlighthouse/config';
export default defineUnlighthouseConfig({
site: 'https://preview.example.com',
cache: true,
scanner: {
include: [
'/',
'/pricing',
'/features/*',
'/blog/*',
'/docs/*',
],
exclude: [
'/admin/*',
'/api/*',
'/*.pdf',
'/search*',
],
samples: 1,
dynamicSampling: 5,
crawler: true,
robotsTxt: true,
sitemap: true,
device: 'mobile',
throttle: true,
},
});
That config is intentionally readable. A reviewer can see that API routes, admin pages, PDFs, and search pages are excluded. They can also see that mobile, throttled Lighthouse behavior is the default. When an AI coding agent changes page structure, this file tells it which routes matter for the quality gate.
For a controlled release gate, prefer include patterns over unrestricted crawling at first. Once you trust the sitemap and crawler behavior, you can remove includes or move them to a nightly scan. A pull request job should be fast enough that engineers do not learn to ignore it.
Understand crawling, sitemaps, and route sampling
The official discovery docs list the order of discovery sources: the specified site, manual URLs, robots.txt, sitemap.xml, crawler-discovered internal links, and static route definitions. Manual urls disable crawler and sitemap scanning. Sitemap behavior also matters: the docs state that when a sitemap exists with over 50 paths, Unlighthouse disables the crawler.
That behavior is helpful for large sites, but it can surprise teams. If your sitemap contains only public marketing pages, crawler disabling is fine. If your sitemap omits important linked pages, you may miss them. Diagnose discovery by inspecting the dashboard route list and running with --debug.
import { defineUnlighthouseConfig } from 'unlighthouse/config';
export default defineUnlighthouseConfig({
site: 'https://docs.example.com',
scanner: {
sitemap: [
'/sitemap.xml',
'/sitemap-docs.xml',
],
crawler: false,
include: [
'/docs/*',
'/reference/*',
],
exclude: [
'/docs/internal/*',
'/reference/generated/*',
],
dynamicSampling: 10,
},
});
Dynamic sampling is the feature that keeps a 20,000-page site from turning every PR into a performance marathon. The docs describe grouping similar paths and sampling a limited number from each group, with scanner.dynamicSampling defaulting to 5. For sites with route definitions, it samples dynamic routes. Without route definitions, it can group by URL fragments.
| Site shape | Suggested discovery mode | Sampling decision |
|---|---|---|
| Small marketing site | Sitemap plus crawler | Disable sampling only if total pages are tiny |
| Large blog | Sitemap, include /blog/* | Keep dynamicSampling around 5 to 10 |
| Ecommerce catalog | Explicit category and product representative URLs | Avoid random sampling for launch-critical SKUs |
| Docs portal | Manual sitemap list and includes | Sample generated reference pages separately |
| Authenticated app | Explicit urls plus auth config | Do not rely on crawler to discover logged-in state |
Sampling is not cheating. It is a statement that template coverage matters more than auditing every interchangeable URL on every commit. For release candidates or migrations, increase samples or run a full nightly scan.
Choose mobile, desktop, throttling, and samples deliberately
The configuration reference lists scanner.device, with mobile as the default and desktop as an alias for a 1350 by 950 viewport. The docs also describe scanner.throttle as an alias for Lighthouse throttling behavior, with throttling disabled by default for local scans. scanner.samples controls how many samples of each route are run to reduce false positives.
Use mobile throttled scans for user-facing performance gates unless you have a specific desktop-only product. Use desktop scans for internal dashboards, enterprise apps, or QA tools where desktop is the primary experience. If you compare mobile and desktop in the same pipeline, split them into separate jobs and separate reports so a desktop pass does not hide a mobile failure.
import { defineUnlighthouseConfig } from 'unlighthouse/config';
const isCi = process.env.CI === 'true';
export default defineUnlighthouseConfig({
site: process.env.SITE_URL ?? 'http://localhost:3000',
scanner: {
device: isCi ? 'mobile' : 'desktop',
throttle: isCi,
samples: isCi ? 3 : 1,
},
puppeteerClusterOptions: {
maxConcurrency: isCi ? 1 : 2,
},
});
The maxConcurrency choice is about measurement stability. Parallel Lighthouse runs are efficient, but CPU contention can make performance scores noisy. For budget-enforced CI, a slower one-at-a-time scan can be more trustworthy than a fast scan that fails randomly under shared runners.
If you need to customize Lighthouse directly, use lighthouseOptions. The docs show options such as onlyCategories, skipAudits, and throttlingMethod. Keep this narrow. The more you customize Lighthouse, the harder it is to compare results with a browser DevTools audit.
import { defineUnlighthouseConfig } from 'unlighthouse/config';
export default defineUnlighthouseConfig({
site: 'https://preview.example.com',
lighthouseOptions: {
onlyCategories: ['performance', 'accessibility', 'seo'],
throttlingMethod: 'devtools',
skipAudits: [
'uses-http2',
],
},
});
Turn scans into CI budgets
unlighthouse-ci is the CI binary. The CI docs say it runs Lighthouse across pages and fails the build if scores drop below a budget. The documented flags include --site, --root, --config-file, --output-path, --budget, --reporter, --build-static, --cache, --no-cache, --desktop, --mobile, --throttle, and LHCI server options.
You can set a single budget from the CLI:
npx unlighthouse-ci --site https://preview.example.com --budget 80
Or use category budgets in config. The configuration reference defines ci.budget as either a number or a record of Lighthouse categories to scores, with values from 1 to 100.
import { defineUnlighthouseConfig } from 'unlighthouse/config';
export default defineUnlighthouseConfig({
site: process.env.SITE_URL ?? 'https://preview.example.com',
ci: {
budget: {
performance: 80,
accessibility: 90,
'best-practices': 85,
seo: 90,
},
buildStatic: true,
},
scanner: {
include: ['/', '/pricing', '/features/*', '/blog/*'],
exclude: ['/admin/*', '/api/*'],
samples: 3,
throttle: true,
device: 'mobile',
},
});
Here is a GitHub Actions job with current action majors and artifact names that do not contain slashes:
name: unlighthouse
on:
pull_request:
push:
branches: [main]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 22.18.0
cache: npm
- run: npm ci
- name: Run Unlighthouse CI
run: npx unlighthouse-ci --site ${{ env.SITE_URL }} --config-file unlighthouse.config.ts --build-static
env:
SITE_URL: https://preview.example.com
- name: Upload Unlighthouse report
if: always()
uses: actions/upload-artifact@v7
with:
name: unlighthouse-report
path: .unlighthouse/client
If your preview URL is produced by a previous deployment step, wait until the host is actually serving the new build before running Unlighthouse. A common false failure is auditing the previous preview because the deployment URL returned before caches were warm or before the app server was ready.
Pick the right reporter and static report output
The CI docs list --reporter options including csv, csvExpanded, json, jsonExpanded, lighthouseServer, and false, with jsonSimple as the default. Output goes to .unlighthouse/ unless --output-path changes it. For human review, --build-static creates a static report client that can be uploaded to a static host or CI artifact.
# Generate a static report in the default .unlighthouse directory.
npx unlighthouse-ci --site https://preview.example.com --build-static
# Save the report elsewhere.
npx unlighthouse-ci \
--site https://preview.example.com \
--output-path ./artifacts/unlighthouse \
--build-static
# Preview a static report locally.
npx sirv-cli .unlighthouse/client
For QA workflows, static reports are useful because they preserve the route list and the page-level scores that caused a budget failure. A single console line saying "budget failed" is not enough for a developer or agent to repair the issue. You want the report attached to the job, ideally with the worst-scoring pages visible.
| Output need | Option | Review workflow |
|---|---|---|
| Machine summary | Default JSON output | Parse in CI or store as artifact |
| Spreadsheet triage | --reporter csvExpanded | Sort by route, category, and score |
| Human route inspection | --build-static | Upload .unlighthouse/client |
| Historical LHCI server | --reporter lighthouseServer plus LHCI flags | Compare trends outside the PR job |
Do not rely only on screenshots of the dashboard. Keep the generated data and static client so failures can be inspected after the CI job completes.
Failure mode: the scan misses the page that regressed
A realistic failure: a team launches a new pricing template. Unlighthouse passes in CI, but production users see a huge layout shift. Investigation shows the pricing page was not in the sitemap, the config used explicit urls, and manual URLs disable crawler and sitemap scanning. The scan worked exactly as configured, but the route set was incomplete.
Diagnose route coverage before tuning Lighthouse. Add a small route inventory check to CI that fails when required paths are absent from the Unlighthouse output or from your own scan manifest. If Unlighthouse data format changes in a future release, keep this as a concept and inspect the generated JSON shape rather than relying on undocumented paths.
import { readFile } from 'node:fs/promises';
const requiredRoutes = ['/', '/pricing', '/features/automation'];
const report = JSON.parse(
await readFile('.unlighthouse/ci-result.json', 'utf8'),
);
const seenRoutes = new Set(
report.routes.map((route) => route.path),
);
for (const route of requiredRoutes) {
if (!seenRoutes.has(route)) {
throw new Error(`Unlighthouse did not audit required route: ${route}`);
}
}
That code is illustrative because teams should verify the generated JSON shape from their selected reporter. The principle is not illustrative: a budget can only protect routes that were actually audited. For critical funnels, route presence is part of the quality gate.
Another failure mode is noisy performance scores from concurrent scans. If the same route fails by a few points on busy CI but passes locally, reduce puppeteerClusterOptions.maxConcurrency, raise scanner.samples, and keep throttling consistent. Do not immediately lower budgets. First determine whether the measurement is unstable or the page is genuinely close to the threshold.
Authenticated and protected pages
The configuration guide includes basic auth and cookie auth examples. Protected scans are possible, but they should be treated like integration tests with secrets. Use a staging account, short-lived credentials, and paths that avoid destructive workflows. Keep login setup explicit; do not ask an AI coding agent to infer authentication by scraping local browser state.
import { defineUnlighthouseConfig } from 'unlighthouse/config';
export default defineUnlighthouseConfig({
site: 'https://staging.example.com',
auth: {
username: process.env.UNLIGHTHOUSE_USER,
password: process.env.UNLIGHTHOUSE_PASSWORD,
},
cookies: [
{
name: 'session',
value: process.env.UNLIGHTHOUSE_SESSION ?? '',
domain: '.example.com',
},
],
scanner: {
urls: [
'/app/dashboard',
'/app/reports',
'/app/settings',
],
crawler: false,
},
});
Be careful with authenticated crawling. A dashboard can contain user-specific links, logout URLs, billing flows, or mutation endpoints disguised as links. Prefer explicit URLs for protected app pages. Lighthouse is still loading the page like a browser, so do not scan workflows that can send email, rotate keys, delete records, or trigger payments.
How to use Unlighthouse with AI coding agents
Agents are good at fixing obvious Lighthouse findings when the failure is concrete: missing alt text, oversized image, render-blocking script, absent meta description, poor heading structure, or a layout shift caused by late-loading dimensions. They are less reliable when the scan boundary is vague. Give the agent the report path, the failing route, the category, and the command to rerun.
Fix the Unlighthouse failure on /pricing.
Use the static report in .unlighthouse/client.
The failing category is accessibility, budget 90.
Run: npx unlighthouse-ci --site http://localhost:3000 --config-file unlighthouse.config.ts --budget 90 --build-static
Do not lower the budget or remove /pricing from the scan.
That prompt prevents the two easiest bad fixes: deleting the route from coverage or lowering the budget. It also gives the agent a local verification command. For larger teams, add a short qa:unlighthouse script to package.json so everyone uses the same flags.
{
"scripts": {
"qa:unlighthouse": "unlighthouse --config-file unlighthouse.config.ts",
"qa:unlighthouse:ci": "unlighthouse-ci --config-file unlighthouse.config.ts --build-static"
},
"devDependencies": {
"unlighthouse": "0.18.0"
}
}
The package pin here is not a universal recommendation, but it is helpful for agent-driven work. If the agent can install dependencies, a pinned runner keeps the report format and CLI behavior stable during a fix.
A rollout plan that avoids noisy gates
Roll out Unlighthouse in three passes. First, run it manually against staging and clean up discovery. Second, add CI in non-blocking mode by uploading the static report but not failing the build. Third, set category budgets for the routes that are stable enough to enforce. Increase coverage gradually.
| Phase | What changes | What to watch |
|---|---|---|
| Route discovery | Add config, includes, excludes, sampling | Missing critical URLs, accidental admin routes |
| Report publishing | Run unlighthouse-ci --build-static and upload output | Whether developers can find the failing page |
| Budget enforcement | Add ci.budget or --budget | Noise from concurrency, throttling drift, unstable previews |
| Expansion | Add more templates and nightly scans | Runtime, duplicate routes, stale sitemap entries |
Performance gates fail politically when they appear random. Make the first enforced gate small, stable, and well explained. A reliable 12-route scan is better than an impressive 1,000-route job that everyone learns to rerun.
Frequently Asked Questions
Is Unlighthouse the same as Lighthouse CI?
No. Lighthouse CI commonly audits selected URLs and provides assertions, budgets, and historical workflows. Unlighthouse focuses on site-wide discovery and bulk Lighthouse scans, with an interactive dashboard and unlighthouse-ci for budgets. They overlap in purpose but differ in ergonomics. Use Unlighthouse when route discovery and page inventory are the hard part. Use Lighthouse CI directly when you already know the exact URLs and need fine-grained assertion configuration.
Why did my scan stop after sitemap URLs?
The discovery docs say that when a sitemap exists with over 50 paths, Unlighthouse disables the crawler. That can be desirable for large sites, but it surprises teams expecting link crawling. Check the debug output and route list. If sitemap coverage is incomplete, provide manual sitemap paths, explicit urls, or adjust crawler settings. For critical pages, assert route presence separately so a missing sitemap entry cannot silently remove coverage.
Should CI run mobile or desktop audits?
Default to mobile for public pages because mobile Lighthouse tends to reveal the harshest performance and accessibility problems. Use desktop for internal tools or products where desktop is the primary user environment. If both experiences matter, run separate jobs with separate reports and budgets. Mixing device modes in one report makes failures harder to explain, especially when an AI agent is asked to fix only one route.
How strict should Unlighthouse budgets be at first?
Start with budgets that catch clear regressions without turning every pull request into a measurement debate. For example, enforce accessibility and SEO earlier because many fixes are deterministic, then tighten performance after route discovery, throttling, samples, and concurrency are stable. Mark any threshold as policy, not truth. The goal is a gate developers trust enough to fix, not a heroic number that gets bypassed.