DEV Community

Sohana Akbar
Sohana Akbar

Posted on

Stop Guessing About Performance: Testing Frontend Performance in CI with Lighthouse CI

Performance isn't a feature you ship once—it's a quality that degrades with every commit if you let it. A new dependency here, an unoptimized image there, and six months later, your app is slower than before you started that "performance sprint" . The fix? Stop treating performance as a post-deployment concern and start enforcing it in your CI pipeline.

Why Lighthouse CI?
Lighthouse CI is a suite of tools from Google that integrates Lighthouse audits directly into your CI/CD workflow . Instead of manually running Lighthouse in DevTools and hoping for the best, you automate performance checks on every pull request.

The benefits are clear:

Catch regressions before they merge — no more discovering performance issues after deployment

Enforce performance budgets — make thresholds that actually matter (unlike suggestions that get ignored)

Get detailed reports alongside every PR with actionable diagnostics

Track metrics over time to understand performance trends

Companies like Halodoc have successfully implemented this approach, automatically blocking merge requests that exceed defined Core Web Vitals thresholds . Before this, performance regressions were caught only after deployment through production monitoring. After implementation? Issues were caught at the merge request stage, saving costly rollbacks and firefighting.

Setting Up Lighthouse CI
Installation
Start by installing Lighthouse CI as a dev dependency:

bash
npm install --save-dev @lhci/cli
Configuration
Create a lighthouserc.js file at your project root:

javascript
module.exports = {
ci: {
collect: {
// Test your local dev server
startServerCommand: 'npm run start',
startServerReadyPattern: 'ready on',
url: ['http://localhost:3000'],
numberOfRuns: 3, // Multiple runs reduce variance
settings: {
preset: 'desktop' // Or 'mobile' for mobile emulation
}
},
assert: {
assertions: {
// Error if performance drops below 90
'categories:performance': ['error', { minScore: 0.9 }],
// Error if accessibility drops below 95
'categories:accessibility': ['error', { minScore: 0.95 }],
// Warn if LCP exceeds 2.5s
'largest-contentful-paint': ['warn', { maxNumericValue: 2500 }],
// Error if CLS exceeds 0.1
'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
// Error if Total Blocking Time exceeds 200ms
'total-blocking-time': ['error', { maxNumericValue: 200 }]
}
},
upload: {
target: 'temporary-public-storage' // Free, publicly accessible reports
}
}
};
You can disable categories you don't care about (like PWA or SEO) to keep the audit focused . For a production setup, real-world teams often enforce strict performance budgets—for example, requiring a Performance score of 90+, LCP under 2.5s, and CLS under 0.1 .

Integrating with GitHub Actions
Here's a complete GitHub Actions workflow that runs Lighthouse CI on every push and pull request:

yaml
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

  - name: Install Lighthouse CI
    run: npm install -g @lhci/cli@0.15.x

  - name: Run Lighthouse CI
    run: lhci autorun
    env:
      LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}

  - name: Upload artifacts
    uses: actions/upload-artifact@v4
    if: always()
    with:
      name: lighthouse-report
      path: .lighthouseci/
      retention-days: 14
Enter fullscreen mode Exit fullscreen mode

Important: The fetch-depth: 20 is critical—shallow clones break LHCI's ancestor detection and cause "Could not find hash" errors .

Setting Up GitHub Status Checks
To get Lighthouse results as status checks on your PRs, you have two options:

Option 1: GitHub App (Recommended)

Install the Lighthouse CI GitHub App

Authorize it for your repository

Copy the token and add it as LHCI_GITHUB_APP_TOKEN in repository secrets

The app tokens expire in 8 hours and use fine-grained permissions—more secure than PATs

Option 2: Personal Access Token

Create a PAT with repo:status scope

Add it as LHCI_GITHUB_TOKEN in repository secrets

With either approach, your PRs will show Lighthouse checks like:

✓ lhci/url/index.html — Performance: 96, Accessibility: 100

Alternative: Community Action
For a simpler setup, you can use the community-built treosh/lighthouse-ci-action:

yaml
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
This action has 1.2k+ stars on GitHub and was built in collaboration with the Lighthouse team .

Testing Preview Deployments
If you're using Vercel, Netlify, or Cloudflare Pages, you can run Lighthouse on preview deployments:

yaml
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
Performance Budgets with budget.json
For finer-grained control, you can use a budget.json file to enforce resource size and timing budgets:

json
[
{
"path": "/*",
"timings": [
{ "metric": "largest-contentful-paint", "budget": 2500 },
{ "metric": "first-contentful-paint", "budget": 1500 },
{ "metric": "total-blocking-time", "budget": 200 }
],
"resourceSizes": [
{ "resourceType": "script", "budget": 300 },
{ "resourceType": "total", "budget": 500 }
],
"resourceCounts": [
{ "resourceType": "third-party", "budget": 10 }
]
}
]
The path property uses robots.txt-style matching, so you can set different budgets for different page types .

Parallel Testing for Multiple URLs
If you have many URLs to test, use a matrix strategy to run them in parallel:

yaml
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
Production-Grade Considerations
Reducing Variance
Lighthouse scores can be noisy. Running multiple audits per URL and taking the median reduces this variance . Configure numberOfRuns: 5 in your config—this provides a stable signal without significantly increasing CI time.

Environment Isolation
For accurate testing, run Lighthouse against your code in an isolated container, not against shared staging environments where other changes can affect results . This ensures performance attribution is reliable.

Start Loose, Tighten Over Time
Don't enforce strict thresholds on day one. Start with budgets loose enough that you're not blocking everything, then tighten them incrementally as your baseline improves . The goal initially is to prevent regressions, not to hit perfect numbers.

The Bottom Line
Performance regressions are inevitable without automated enforcement. A performance budget without a failing CI check is just a suggestion . By integrating Lighthouse CI into your CI/CD pipeline, you transform performance from a post-deployment afterthought into a pre-merge requirement—just like test coverage and security scanning.

Your users won't thank you for making the site fast. But they'll definitely notice if you make it slow.

Top comments (0)