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.

Lighthouse CI gating flow A flow showing lhci collect run against a preview URL, then lhci assert on budgets, branching to promote the build on pass or block promotion on fail. lhci collect against preview URL lhci assert CWV + SEO budgets Promote build Block promotion pass fail

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