Lighthouse CI Configuration & Storage

Storage backends that retain every Lighthouse run unbounded degrade query performance past 10k builds, and non-deterministic collection settings produce the flaky scores that erode a team's trust in the gate. This is the configuration layer of the Lighthouse CI & WebPageTest Integration reference: it turns subjective audits into an enforceable engineering contract by pinning collection settings, choosing a durable result store, and wiring assertion thresholds that block suboptimal code before it reaches production.

The job splits into three coupled concerns — how runs are collected (determinism), where results are persisted (storage), and what thresholds fail the build (assertions). Get the first wrong and the other two inherit the noise. This page is the authoritative spec for all three.

Architecture Overview

A Lighthouse CI pipeline moves a build through collection, assertion, and upload stages, each reading from the same lighthouserc configuration. The diagram below shows how the stages connect and where the storage backend attaches.

Lighthouse CI collect → assert → upload pipeline A pull request triggers the collect stage which runs three Lighthouse audits, the assert stage which evaluates budget thresholds and either passes or blocks the merge, and the upload stage which persists artifacts to a chosen storage backend. Pull Request collect numberOfRuns: 3 throttling: simulate median report kept assert LCP < 2500 ms JS < 200 KB exit 1 on breach merge allowed status check ✓ upload → storage backend LHCI server SQLite / Postgres
The collect stage feeds median metrics to assert; a pass releases the merge while upload persists every run to the storage backend for trend analysis.

Prerequisites & Environment

Lighthouse CI requires Node.js 18 or newer and a Chrome/Chromium binary available on the runner. Install the CLI as a dev dependency rather than globally so the version is pinned in package-lock.json and reproducible across machines.

  • @lhci/cli ≥ 0.13 — the collect/assert/upload toolchain. Pin the exact minor version; assertion semantics shift between minors.
  • Node.js ≥ 18, Chrome ≥ 120 — GitHub-hosted ubuntu-latest runners ship a compatible Chrome.
  • Runner specs — minimum 2 vCPU / 7 GB RAM. The default GitHub runner is sufficient for simulated throttling; provided throttling on a noisy 2-core box is the single most common source of variance, covered in Reducing Lighthouse CI Variance in Staging.

Map sensitive credentials and dynamic endpoints through environment variables so nothing is hardcoded:

  • LHCI_TOKEN — write token for the centralized LHCI server.
  • LHCI_SERVER_BASE_URL — endpoint of the persistent storage backend.
  • LHCI_BUILD_CONTEXT__CURRENT_HASH — Git SHA used for historical correlation against your Historical Baseline Calibration store.

Configuration Reference

Select the configuration format by environment-injection need. Use lighthouserc.json for static, version-controlled baselines that guarantee reproducibility; use lighthouserc.js when you need dynamic environment-variable resolution or runtime URL generation from CI context. The annotated block below is the authoritative spec — every parameter is explained inline.

{
  "ci": {
    "collect": {
      "url": ["https://staging.example.com/", "https://staging.example.com/checkout"],
      "numberOfRuns": 3,
      "settings": {
        "preset": "desktop",
        "throttlingMethod": "simulate",
        "throttling": { "cpuSlowdownMultiplier": 4, "requestLatencyMs": 150 },
        "chromeFlags": "--no-sandbox --disable-dev-shm-usage"
      }
    },
    "assert": {
      "assertions": {
        "categories:performance": ["error", { "minScore": 0.9 }],
        "metric-lcp": ["error", { "maxNumericValue": 2500 }],
        "metric-cls": ["error", { "maxNumericValue": 0.1 }],
        "metric-tbt": ["error", { "maxNumericValue": 200 }],
        "resource-summary:script:size": ["error", { "maxNumericValue": 200000 }]
      }
    },
    "upload": {
      "target": "lhci",
      "serverBaseUrl": "${LHCI_SERVER_BASE_URL}",
      "token": "${LHCI_TOKEN}"
    }
  }
}

numberOfRuns: 3 is the floor for a stable median; fewer runs leak single-sample noise into the gate. throttlingMethod: simulate keeps timings deterministic by modelling the network in software rather than depending on the runner's real bandwidth. The assert block is evaluated against the median report, and any error-level breach exits the process non-zero.

Step-by-Step Implementation

  1. Install and scaffold. Add the CLI and a config file to the repository root.

    npm install --save-dev @lhci/[email protected]
    npx lhci healthcheck --fatal

    Expected output: ✅ Healthcheck passed! confirming Chrome, config, and server reachability.

  2. Run a local collection against a built preview to verify determinism before wiring CI.

    npm run build && npx lhci autorun --collect.url=http://localhost:8080/

    Expected tail: Done running Lighthouse! followed by All results processed! and an assertion summary table.

  3. Inspect the median report to confirm the metrics you intend to gate are present and reasonable, then commit lighthouserc.json.

Threshold Calibration

Do not copy the defaults above into production untouched — derive each ceiling from your own field data. Pull the P75 of each metric from your RUM or CrUX dataset, then set the lab assertion 10–15% tighter to absorb the lab-to-field gap. The matrix below shows representative starting points; calibrate against the methodology in Percentile-Based Threshold Tuning.

Device class Connection profile LCP ceiling (P75) TBT ceiling (P75) Script budget
Desktop Cable / Fiber 2000 ms 150 ms 200 KB
High-end mobile 4G / LTE 2500 ms 200 ms 170 KB
Mid-range mobile Fast 3G 3500 ms 350 ms 150 KB

Set assertion level to warn for any metric still being calibrated and error only once the threshold has held for two consecutive weeks of baselines, so the gate earns trust before it blocks merges.

CI Enforcement Snippet

This GitHub Actions job is copy-paste ready: it builds, runs Lighthouse CI, and surfaces a required status check that branch protection can gate on.

name: Performance Gating
on:
  pull_request:
    branches: [main]

jobs:
  lighthouse-ci:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    concurrency:
      group: lhci-${{ github.ref }}
      cancel-in-progress: true
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"
      - run: npm ci
      - run: npm run build
      - name: Run Lighthouse CI
        run: npx lhci autorun
        env:
          LHCI_TOKEN: ${{ secrets.LHCI_TOKEN }}
          LHCI_SERVER_BASE_URL: ${{ secrets.LHCI_SERVER_BASE_URL }}
      - name: Upload reports
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: lighthouse-reports
          path: .lighthouseci/

Scale this across viewports and routes with GitHub Actions Performance Matrices, and require the lighthouse-ci check in branch protection so a breach is unmergeable. For the per-PR comment workflow, see Running Lighthouse CI on Every Pull Request.

Storage Backend Selection

Choose a target by retention need and query volume. The filesystem target suits ephemeral local testing. The LHCI server — backed by SQLite for small teams or PostgreSQL past ~10k builds — provides dashboards, an API, and trend visualization. Route artifacts to object storage (S3/GCS) only when you need raw report retention beyond the server's pruning window. Configure automated pruning to retain the latest 50 builds per branch or 30 days, whichever is larger; unbounded SQLite is the classic cause of slow dashboards.

Troubleshooting & Edge Cases

  • Headless Chrome crashes in CI → add --no-sandbox --disable-dev-shm-usage to chromeFlags; the default /dev/shm is too small on hosted runners.
  • Assertion drift after a third-party update → pin vendor versions or widen the tolerance window; cross-check with Third-Party Script Constraints.
  • LHCI_TOKEN expiry breaks nightly baselines → rotate the token as a repository secret and re-run lhci healthcheck.
  • Flaky LCP between runs → raise numberOfRuns to 5 and switch to throttlingMethod: simulate; real-network throttling on shared runners is non-deterministic.
  • Storage quota exceeded → enable pruning (--server.storage.sqlDeleteRowsBatchSize) or migrate SQLite → PostgreSQL.
  • Timeouts on slow staging → raise timeout-minutes and warm the cache with a curl before lhci collect.

Frequently Asked Questions

How many Lighthouse runs are enough to trust the median?

Three runs is the practical floor and five is the comfortable default for noisy environments. Lighthouse keeps the median report, so odd counts avoid tie-breaking. If a metric still swings more than 10% across five runs, the variance is environmental — fix the runner before tightening the threshold.

Should I store results in SQLite or PostgreSQL?

SQLite is fine below roughly 10,000 builds and for single-team setups. Past that, dashboard queries slow noticeably and concurrent writes contend; migrate to PostgreSQL. Either way, enable pruning so the store does not grow without bound.

Why does my assertion pass locally but fail in CI?

Almost always throttling method. Local runs often use provided throttling on a fast machine, while CI should use simulate for determinism. Align both to simulate and the gap usually disappears.