Integrating Lighthouse CI Into Headless Deploy Pipelines
Run Lighthouse CI against each preview deployment, assert Core Web Vitals and SEO budgets, and gate promotion so a performance or crawlability regression can never reach production.
When to Use This Approach
Add Lighthouse CI to your pipeline when any of the following apply:
- Core Web Vitals regress between releases and you only notice weeks later in field data from Search Console.
- You want an enforceable performance and SEO budget per pull request rather than an occasional manual audit.
- Your headless frontend ships preview deployments per branch, giving you a stable URL to measure before promotion.
Implementation Steps
Step 1: Install Lighthouse CI
Add the CLI as a dev dependency and scaffold a configuration file at the repository root.
npm install --save-dev @lhci/cli
# create lighthouserc.js at the repo root (see Step 3 for its contents)
npx lhci healthcheck --fatal
Validation: npx lhci healthcheck --fatal exits 0 and reports that Chrome is installed and the config is discoverable. A non-zero exit means Chrome headless is missing from the runner.
Step 2: Collect Against the Preview URL
Point the collect step at the per-pull-request preview deployment so Lighthouse measures the real edge and network path. Measuring localhost hides CDN caching and compression effects that shape edge caching behavior for SEO.
# $PREVIEW_URL is exported by your host's deploy step
npx lhci collect \
--url="$PREVIEW_URL" \
--url="$PREVIEW_URL/blog/example-post" \
--numberOfRuns=3
Validation: The command writes reports to .lighthouseci/. Confirm one report exists per URL per run (three runs = three JSON files per URL) before asserting.
Step 3: Define Assertion Budgets
Declare error-level assertions for the metrics that move rankings. Asserting on the median of three runs absorbs network variance.
// lighthouserc.js
module.exports = {
ci: {
assert: {
assertions: {
'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
'total-blocking-time': ['error', { maxNumericValue: 200 }],
'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
'render-blocking-resources': ['error', { maxLength: 0 }],
'categories:seo': ['error', { minScore: 1 }],
},
},
},
};
Validation: Run npx lhci assert locally against a known-good build; it must exit 0. Temporarily tighten a budget to force a failure and confirm the exit code becomes non-zero.
Step 4: Upload and Store Results
Persist reports so each run is compared against a rolling baseline, not just a fixed number. Use a hosted Lighthouse CI server for history, or temporary public storage for quick PR links.
# Option A: temporary public storage (report URL printed in logs)
npx lhci upload --target=temporary-public-storage
# Option B: your own Lighthouse CI server
npx lhci upload \
--target=lhci \
--serverBaseUrl="$LHCI_SERVER_URL" \
--token="$LHCI_BUILD_TOKEN"
Validation: The upload step prints a report URL (Option A) or returns HTTP 200 from the server (Option B). Open the link and confirm the run’s scores match the local assert output.
Step 5: Fail the Job and Block Promotion
Wire the assertion into CI so a breach fails the job, and make the promote job depend on it. A non-zero exit from lhci autorun stops the pipeline.
# .github/workflows/deploy.yml
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx lhci autorun --collect.url=${{ env.PREVIEW_URL }}
promote:
needs: lighthouse # skipped entirely if lighthouse fails
runs-on: ubuntu-latest
steps:
- run: ./scripts/promote-to-production.sh
Validation: Open a pull request that intentionally regresses LCP (add a large synchronous script). The lighthouse job must fail and the promote job must show as skipped.
SEO Impact Summary
| Signal | What improves | What breaks if misconfigured |
|---|---|---|
| Core Web Vitals | Regressions are caught per PR before field data degrades | Warn-only assertions let regressions ship silently |
| Crawlability | categories:seo and render-blocking assertions guard indexable HTML |
Testing localhost hides edge caching and misses real TTFB |
| Release confidence | A failing gate blocks promotion automatically | A promote job without needs: runs regardless of the result |
| Trend visibility | Stored history flags gradual drift | No upload means every run is judged in isolation |
Measurable signals to watch:
- CrUX / Search Console Core Web Vitals: the share of “Good” URLs should hold or rise release over release.
- Pull requests blocked by the Lighthouse job: a healthy number early, trending down as budgets stabilize.
- Median LCP and TBT in stored Lighthouse history should stay flat or improve across releases.
Edge Cases and Gotchas
The preview must be ready before collect runs.
If the Lighthouse job starts before the deployment finishes, collect measures a 404 or a loading shell and reports meaningless scores. Poll the preview URL for a 200 (and expected content) before running collect, or use your host’s “deployment ready” event to trigger the job.
Password-protected previews break the collect step. Many hosts gate preview URLs behind authentication. Lighthouse then measures the login wall. Use a preview-bypass token via a request header, or publish the diagnostic build to an unprotected preview channel dedicated to CI.
Single runs are noisy.
One run captures cold caches and network jitter. Always set numberOfRuns to at least 3 and assert on the aggregated median so a transient spike does not fail an otherwise-healthy build.
Green Lighthouse can still hide client-only content. Lighthouse executes JavaScript, so a client-rendered page can pass every budget while shipping an empty server HTML shell. Pair this gate with the checks in diagnosing render-blocking JavaScript in headless frontends to confirm crawler-visible content exists without script execution.
Frequently Asked Questions
How do I fail CI on a Lighthouse regression?
Declare error-level assertions in lighthouserc.js. When a metric breaches its budget, lhci assert exits non-zero, which fails the CI job. Make the promote job depend on the Lighthouse job with needs: so a failure blocks promotion automatically rather than requiring a manual check.
Can lhci test a preview deploy URL?
Yes, and it should. Pass the per-pull-request preview URL to collect.url so Lighthouse measures the deployed build over the real edge and network path rather than a local dev server. Wait for the preview to return 200 with expected content before running collect, or you will measure a loading shell.
Which assertions matter for SEO?
Assert the three Core Web Vitals proxies — largest-contentful-paint, total-blocking-time, and cumulative-layout-shift — plus categories:seo at minScore 1 and render-blocking-resources at maxLength 0. Those catch the field-metric regressions and crawlability issues that actually affect rankings.
Should the whole team be blocked by one flaky metric?
No. Flakiness usually means single-run variance or an unready preview. Fix it by running three collections and asserting on the median, and by gating collect on a readiness check. Reserve error for stable, meaningful budgets and use warn for metrics you are still calibrating.
Part of: Debugging & Diagnostics for Headless Rendering
Related
- Diagnosing Render-Blocking JavaScript in Headless Frontends — confirm crawler-visible content exists even when Lighthouse scores are green
- Edge Caching Behavior for SEO — why measuring the deployed preview, not localhost, reflects real crawler TTFB