Lighthouse CI with GitHub Actions: Complete Setup Guide

Step-by-step tutorial for running Lighthouse CI in GitHub Actions. Includes YAML workflows, status checks, and PR comments.
Harlan WiltonHarlan Wilton Published

Run Lighthouse audits on every push and pull request using GitHub Actions. This guide covers basic setup through advanced configurations with status checks and PR comments.

Requirements: LHCI 0.15.x requires Node 18+. GitHub Actions Ubuntu runners include Chrome pre-installed at /usr/bin/google-chrome.

Basic Workflow

Create .github/workflows/lighthouse.yml:

name: Lighthouse CI
on: [push]

jobs:
  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 20 # Required for base branch comparison
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci && npm run build
      - run: npm install -g @lhci/cli@0.15.x
      - run: lhci autorun
Critical: Always set fetch-depth: 20 or higher. Shallow clones break LHCI's ancestor detection and cause "Could not find hash" errors.

Add lighthouserc.js to your repo root:

module.exports = {
  ci: {
    collect: {
      staticDistDir: './dist',
    },
    upload: {
      target: 'temporary-public-storage',
    },
  },
}

Testing a Live URL

If your site is already deployed:

module.exports = {
  ci: {
    collect: {
      url: ['https://example.com/', 'https://example.com/about'],
    },
    upload: {
      target: 'temporary-public-storage',
    },
  },
}

Testing with a Local Server

For apps that need a running server:

module.exports = {
  ci: {
    collect: {
      startServerCommand: 'npm run start',
      url: ['http://localhost:3000/'],
    },
    upload: {
      target: 'temporary-public-storage',
    },
  },
}

LHCI starts your server, waits for it to be ready, runs audits, then shuts it down.

GitHub Status Checks

Add Lighthouse results as GitHub status checks on PRs.

  1. Install the Lighthouse CI GitHub App
  2. Authorize it for your repository
  3. Copy the token from the authorization page
  4. Add it as a repository secret named LHCI_GITHUB_APP_TOKEN
Why GitHub App over PAT?GitHub App tokens expire in 8 hours vs PATs which can be indefinite. Apps also use fine-grained permissions scoped to status checks only, making them more secure for public repos.

Update your workflow:

name: Lighthouse CI
on: [push, pull_request]

jobs:
  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci && npm run build
      - run: npm install -g @lhci/cli@0.15.x
      - run: lhci autorun
        env:
          LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}

The ref: ${{ github.event.pull_request.head.sha }} ensures the correct commit is checked out for PRs.

Option 2: Personal Access Token

  1. Create a personal access token with repo:status scope
  2. Add it as a secret named LHCI_GITHUB_TOKEN
- run: lhci autorun
  env:
    LHCI_GITHUB_TOKEN: ${{ secrets.LHCI_GITHUB_TOKEN }}

Adding Assertions

Fail the build if performance drops below thresholds:

module.exports = {
  ci: {
    collect: {
      staticDistDir: './dist',
    },
    assert: {
      preset: 'lighthouse:recommended',
    },
    upload: {
      target: 'temporary-public-storage',
    },
  },
}

Or with custom thresholds:

module.exports = {
  ci: {
    collect: {
      staticDistDir: './dist',
    },
    assert: {
      assertions: {
        'categories:performance': ['error', { minScore: 0.9 }],
        'categories:accessibility': ['error', { minScore: 0.9 }],
        'first-contentful-paint': ['warn', { maxNumericValue: 2000 }],
      },
    },
    upload: {
      target: 'temporary-public-storage',
    },
  },
}

Using treosh/lighthouse-ci-action

The lighthouse-ci-action (1.2k+ stars) was built in collaboration with the Lighthouse team and simplifies the workflow:

name: Lighthouse CI
on: [push]

jobs:
  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run Lighthouse
        uses: treosh/lighthouse-ci-action@v12
        with:
          urls: |
            https://example.com/
            https://example.com/about
          budgetPath: ./budget.json
          uploadArtifacts: true

With assertions:

- uses: treosh/lighthouse-ci-action@v12
  with:
    urls: https://example.com/
    configPath: ./lighthouserc.js

Testing Preview Deploys

For Vercel, Netlify, or Cloudflare Pages preview deployments:

name: Lighthouse on Preview
on: deployment_status

jobs:
  lighthouse:
    if: github.event.deployment_status.state == 'success'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm install -g @lhci/cli@0.15.x
      - run: |
          lhci autorun \
            --collect.url=${{ github.event.deployment_status.target_url }} \
            --upload.target=temporary-public-storage

Multiple URLs with Matrix

Test multiple pages in parallel:

name: Lighthouse CI
on: [push]

jobs:
  lighthouse:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        url:
          - https://example.com/
          - https://example.com/pricing
          - https://example.com/docs
    steps:
      - uses: actions/checkout@v4
      - run: npm install -g @lhci/cli@0.15.x
      - run: |
          lhci autorun \
            --collect.url=${{ matrix.url }} \
            --upload.target=temporary-public-storage

Uploading Reports as Artifacts

Save HTML reports for later review:

- run: lhci autorun
- uses: actions/upload-artifact@v4
  if: always()
  with:
    name: lighthouse-report
    path: .lighthouseci/
    retention-days: 14

Running Multiple Times

Reduce variance by running multiple audits per URL:

module.exports = {
  ci: {
    collect: {
      numberOfRuns: 5,
      url: ['https://example.com/'],
    },
    upload: {
      target: 'temporary-public-storage',
    },
  },
}

LHCI uses the median result for assertions. Google's research shows the median of 5 runs is twice as stable as a single run. Even 3 runs reduces variance by 37%.

Caching Chrome

Speed up workflows by caching Puppeteer's Chrome:

- uses: actions/cache@v4
  with:
    path: ~/.cache/puppeteer
    key: ${{ runner.os }}-puppeteer-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-puppeteer-

Token Security

The LHCI_TOKEN (build token for LHCI Server) is write-only and additive — it cannot read or destroy historical data. This makes it safe to use in public repos as a secret without risk of data exfiltration.

Full Production Workflow

Complete example with all features:

name: Lighthouse CI
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}
          fetch-depth: 20 # Required for ancestor comparison

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - run: npm ci
      - run: npm run build

      - run: npm install -g @lhci/cli@0.15.x

      - run: lhci autorun
        env:
          LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}

      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: lighthouse-report
          path: .lighthouseci/
          retention-days: 14

With lighthouserc.js:

module.exports = {
  ci: {
    collect: {
      staticDistDir: './dist',
      numberOfRuns: 3,
    },
    assert: {
      preset: 'lighthouse:recommended',
      assertions: {
        'categories:performance': ['error', { minScore: 0.8 }],
        'categories:accessibility': ['error', { minScore: 0.9 }],
      },
    },
    upload: {
      target: 'temporary-public-storage',
    },
  },
}

Next Steps

When Budgets Fail

If your assertions fail, use these guides to fix the underlying issues: