Lighthouse CI with GitLab CI/CD: Setup Guide

Configure Lighthouse CI for GitLab pipelines. Includes .gitlab-ci.yml examples, merge request integration, and artifact storage.
Harlan WiltonHarlan Wilton Published

Lighthouse CI integrates seamlessly with GitLab CI/CD to run performance audits on every merge request. This guide covers setup, configuration, and best practices for GitLab pipelines.

Quick Start

Basic .gitlab-ci.yml to audit a static site:

lighthouse:
  image: cypress/browsers:node-22.12.0-chrome-131.0.6778.204-1-ff-134.0-edge-131.0.2903.112-1
  stage: test
  script:
    - npm install -g @lhci/cli@0.15.x
    - lhci autorun
  artifacts:
    when: always
    paths:
      - .lighthouseci/
    expire_in: 7 days

Create lighthouserc.js:

module.exports = {
  ci: {
    collect: {
      staticDistDir: './dist',
      chromePath: '/usr/bin/google-chrome',
      chromeFlags: '--no-sandbox'
    },
    upload: {
      target: 'temporary-public-storage'
    }
  }
}

Why --no-sandbox?

GitLab CI runners require Chrome's --no-sandbox flag because Docker's default seccomp profile blocks the syscalls Chrome needs to create its own namespace sandbox. The container itself provides isolation, creating a conflict.

Without it, Chrome fails to launch:

Failed to launch Chrome: spawn EACCES

Always include chromeFlags: '--no-sandbox' in your configuration.

Security note: Only use --no-sandbox in trusted CI environments running your own code. If auditing untrusted websites, malicious pages could escape the container. For maximum security, use Jess Frazelle's Chrome seccomp profile which allows only the specific syscalls Chrome needs.

Testing Different Targets

Static Build Output

Audit files in dist/ or build/:

lighthouse:
  image: cypress/browsers:node-22.12.0-chrome-131.0.6778.204-1-ff-134.0-edge-131.0.2903.112-1
  stage: test
  script:
    - npm ci
    - npm run build
    - npm install -g @lhci/cli@0.15.x
    - lhci autorun
  artifacts:
    paths:
      - .lighthouseci/
// lighthouserc.js
module.exports = {
  ci: {
    collect: {
      staticDistDir: './dist',
      chromePath: '/usr/bin/google-chrome',
      chromeFlags: '--no-sandbox'
    }
  }
}

Live URLs

Test production or staging URLs:

module.exports = {
  ci: {
    collect: {
      url: [
        'https://example.com',
        'https://example.com/about',
        'https://example.com/products'
      ],
      chromePath: '/usr/bin/google-chrome',
      chromeFlags: '--no-sandbox'
    }
  }
}

Local Development Server

Start server, run audits, shut down:

module.exports = {
  ci: {
    collect: {
      startServerCommand: 'npm run serve',
      startServerReadyPattern: 'Server listening on',
      url: ['http://localhost:8080'],
      chromePath: '/usr/bin/google-chrome',
      chromeFlags: '--no-sandbox'
    }
  }
}

The CLI automatically kills the server after audits complete.

Adding Assertions

Fail the pipeline if performance budgets aren't met:

module.exports = {
  ci: {
    collect: {
      staticDistDir: './dist',
      chromePath: '/usr/bin/google-chrome',
      chromeFlags: '--no-sandbox'
    },
    assert: {
      assertions: {
        'categories:performance': ['error', { minScore: 0.9 }],
        'categories:accessibility': ['error', { minScore: 0.9 }],
        'categories:best-practices': ['error', { minScore: 0.9 }],
        'categories:seo': ['error', { minScore: 0.9 }],
        'first-contentful-paint': ['error', { maxNumericValue: 2000 }],
        'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
        'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
        'total-blocking-time': ['error', { maxNumericValue: 300 }]
      }
    },
    upload: {
      target: 'temporary-public-storage'
    }
  }
}

Pipeline fails if any assertion fails. See budgets for assertion syntax.

Storing Reports as Artifacts

Save HTML reports for download:

lighthouse:
  image: cypress/browsers:node-22.12.0-chrome-131.0.6778.204-1-ff-134.0-edge-131.0.2903.112-1
  stage: test
  script:
    - npm install -g @lhci/cli@0.15.x
    - lhci autorun --upload.target=filesystem --upload.outputDir=./lhci-reports
  artifacts:
    when: always
    paths:
      - lhci-reports/
      - .lighthouseci/
    expire_in: 30 days

Reports appear in GitLab's merge request artifacts.

Testing Review Apps

Audit ephemeral Review Apps using GitLab environment variables:

lighthouse:
  image: cypress/browsers:node-22.12.0-chrome-131.0.6778.204-1-ff-134.0-edge-131.0.2903.112-1
  stage: test
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
  script:
    - npm install -g @lhci/cli@0.15.x
    - lhci autorun --collect.url=$CI_ENVIRONMENT_URL
  environment:
    name: review/$CI_COMMIT_REF_NAME
    url: https://$CI_COMMIT_REF_SLUG.review.example.com

Or configure in lighthouserc.js:

module.exports = {
  ci: {
    collect: {
      url: [process.env.CI_ENVIRONMENT_URL],
      chromePath: '/usr/bin/google-chrome',
      chromeFlags: '--no-sandbox'
    }
  }
}

Reducing Variance with Multiple Runs

Run Lighthouse multiple times and use median values:

module.exports = {
  ci: {
    collect: {
      numberOfRuns: 5,
      staticDistDir: './dist',
      chromePath: '/usr/bin/google-chrome',
      chromeFlags: '--no-sandbox'
    }
  }
}

Increases job duration but reduces false negatives from variance. Google's research shows 5 runs is twice as stable as 1 run. GitLab runners are generally stable, so 3 runs (37% variance reduction) is usually sufficient.

Hardware requirements: LHCI needs minimum 2 cores and 4GB RAM. Avoid burstable instances (AWS t-series) as CPU throttling causes score inconsistency.

Full Production Pipeline

Complete example with build, test, and artifact storage:

stages:
  - build
  - test

variables:
  NODE_ENV: production

build:
  stage: build
  image: node:20
  script:
    - npm ci
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 1 hour

lighthouse:
  stage: test
  image: cypress/browsers:node-22.12.0-chrome-131.0.6778.204-1-ff-134.0-edge-131.0.2903.112-1
  dependencies:
    - build
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH == "main"'
  script:
    - npm install -g @lhci/cli@0.15.x
    - lhci autorun
  artifacts:
    when: always
    paths:
      - .lighthouseci/
      - lhci-reports/
    expire_in: 30 days
// lighthouserc.js
module.exports = {
  ci: {
    collect: {
      staticDistDir: './dist',
      numberOfRuns: 3,
      chromePath: '/usr/bin/google-chrome',
      chromeFlags: '--no-sandbox'
    },
    assert: {
      preset: 'lighthouse:recommended',
      assertions: {
        'categories:performance': ['error', { minScore: 0.9 }],
        'uses-responsive-images': 'off',
        'offscreen-images': 'off'
      }
    },
    upload: {
      target: 'filesystem',
      outputDir: './lhci-reports'
    }
  }
}

Lighthouse CI Server Integration

Upload to a persistent Lighthouse CI server for historical tracking:

lighthouse:
  image: cypress/browsers:node-22.12.0-chrome-131.0.6778.204-1-ff-134.0-edge-131.0.2903.112-1
  stage: test
  script:
    - npm install -g @lhci/cli@0.15.x
    - lhci autorun --upload.serverBaseUrl=$LHCI_SERVER_URL --upload.token=$LHCI_BUILD_TOKEN

Set LHCI_SERVER_URL and LHCI_BUILD_TOKEN as CI/CD variables in GitLab project settings. See server setup for details.

Troubleshooting

Chrome Won't Launch

Error:

Failed to launch Chrome: spawn /usr/bin/google-chrome ENOENT

Fix: Use cypress/browsers image which includes Chrome, or install Chrome manually:

script:
  - apt-get update
  - apt-get install -y wget gnupg
  - wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | gpg --dearmor -o /usr/share/keyrings/google-chrome.gpg
  - echo "deb [signed-by=/usr/share/keyrings/google-chrome.gpg] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google.list
  - apt-get update
  - apt-get install -y google-chrome-stable
  - npm install -g @lhci/cli@0.15.x
  - lhci autorun

Sandbox Errors

Error:

Failed to move to new namespace: PID namespaces supported, Network namespace supported, but failed: errno = Operation not permitted

Fix: Add --no-sandbox to chrome flags:

chromeFlags: '--no-sandbox'

Out of Memory

Error:

FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory

Fix: Increase memory in .gitlab-ci.yml:

variables:
  NODE_OPTIONS: --max-old-space-size=4096

Or reduce numberOfRuns:

collect: {
  numberOfRuns: 1
}

Shared Memory Issues

Error:

Browser tab has unexpectedly crashed

Fix: Docker's default 64MB /dev/shm is insufficient for Chrome. Either use --disable-dev-shm-usage flag (writes to disk, slower) or increase shared memory:

services:
  - name: docker:dind
    command: [--shm-size=2g]

Job Timeout

GitLab default timeout is 1 hour. For numberOfRuns: 5, jobs may timeout. Reduce runs or increase timeout:

lighthouse:
  timeout: 2 hours

Reports Not Generated

Ensure upload.target is set:

upload: {
  target: 'filesystem',
  outputDir: './lhci-reports'
}

And artifacts path matches:

artifacts:
  paths:
    - lhci-reports/

Next Steps

When Budgets Fail

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