Lighthouse Audits
CLI Quick Start
Install
npm install -g lighthouse
Basic audit
lighthouse https://example.com
Mobile performance only (faster)
lighthouse https://example.com --preset=perf --form-factor=mobile
Output JSON for parsing
lighthouse https://example.com --output=json --output-path=./report.json
Output HTML report
lighthouse https://example.com --output=html --output-path=./report.html
Common Flags
--preset=perf # Performance only (skip accessibility, SEO, etc.) --form-factor=mobile # Mobile device emulation (default) --form-factor=desktop # Desktop --throttling-method=devtools # More accurate throttling --only-categories=performance,accessibility # Specific categories --chrome-flags="--headless" # Headless Chrome
Performance Budgets
Create budget.json :
[ { "resourceSizes": [ { "resourceType": "script", "budget": 200 }, { "resourceType": "image", "budget": 300 }, { "resourceType": "stylesheet", "budget": 50 }, { "resourceType": "total", "budget": 500 } ], "resourceCounts": [{ "resourceType": "third-party", "budget": 5 }], "timings": [ { "metric": "interactive", "budget": 3000 }, { "metric": "first-contentful-paint", "budget": 1500 }, { "metric": "largest-contentful-paint", "budget": 2500 } ] } ]
Run with budget:
lighthouse https://example.com --budget-path=./budget.json
Node API
import lighthouse from 'lighthouse' import * as chromeLauncher from 'chrome-launcher'
async function runAudit(url) { const chrome = await chromeLauncher.launch({ chromeFlags: ['--headless'] })
const result = await lighthouse(url, { port: chrome.port, onlyCategories: ['performance'], formFactor: 'mobile', throttling: { cpuSlowdownMultiplier: 4, }, })
await chrome.kill()
const { performance } = result.lhr.categories const { 'largest-contentful-paint': lcp } = result.lhr.audits
return { score: Math.round(performance.score * 100), lcp: lcp.numericValue, } }
GitHub Actions
.github/workflows/lighthouse.yml
name: Lighthouse
on: pull_request: push: branches: [main]
jobs: lighthouse: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- name: Build site
run: npm ci && npm run build
- name: Run Lighthouse
uses: treosh/lighthouse-ci-action@v11
with:
urls: |
http://localhost:3000
http://localhost:3000/about
budgetPath: ./budget.json
uploadArtifacts: true
temporaryPublicStorage: true
env:
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
Lighthouse CI (LHCI)
For full CI integration with historical tracking:
Install
npm install -g @lhci/cli
Initialize config
lhci wizard
Creates lighthouserc.js :
module.exports = { ci: { collect: { url: ['http://localhost:3000/', 'http://localhost:3000/about'], startServerCommand: 'npm run start', numberOfRuns: 3, }, assert: { assertions: { 'categories:performance': ['error', { minScore: 0.9 }], 'categories:accessibility': ['warn', { minScore: 0.9 }], 'first-contentful-paint': ['error', { maxNumericValue: 1500 }], 'largest-contentful-paint': ['error', { maxNumericValue: 2500 }], 'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }], }, }, upload: { target: 'temporary-public-storage', // or 'lhci' for self-hosted }, }, }
Run:
lhci autorun
Parse JSON Report
import fs from 'fs'
const report = JSON.parse(fs.readFileSync('./report.json'))
// Overall scores (0-1, multiply by 100 for percentage) const scores = { performance: report.categories.performance.score, accessibility: report.categories.accessibility.score, seo: report.categories.seo.score, }
// Core Web Vitals const vitals = { lcp: report.audits['largest-contentful-paint'].numericValue, cls: report.audits['cumulative-layout-shift'].numericValue, fcp: report.audits['first-contentful-paint'].numericValue, tbt: report.audits['total-blocking-time'].numericValue, }
// Failed audits const failed = Object.values(report.audits) .filter((a) => a.score !== null && a.score < 0.9) .map((a) => ({ id: a.id, score: a.score, title: a.title }))
Compare Builds
Save baseline
lighthouse https://prod.example.com --output=json --output-path=baseline.json
Run on PR
lighthouse https://preview.example.com --output=json --output-path=pr.json
Compare (custom script)
node compare-reports.js baseline.json pr.json
Simple comparison script:
const baseline = JSON.parse(fs.readFileSync(process.argv[2])) const pr = JSON.parse(fs.readFileSync(process.argv[3]))
const metrics = ['largest-contentful-paint', 'cumulative-layout-shift', 'total-blocking-time']
metrics.forEach((metric) => {
const base = baseline.audits[metric].numericValue
const current = pr.audits[metric].numericValue
const diff = (((current - base) / base) * 100).toFixed(1)
const emoji = current <= base ? '✅' : '❌'
console.log(${emoji} ${metric}: ${diff}% (${base.toFixed(0)} → ${current.toFixed(0)}))
})
Troubleshooting
Issue Solution
Inconsistent scores Run multiple times (--number-of-runs=3 ), use median
Chrome not found Set CHROME_PATH env var
Timeouts Increase with --max-wait-for-load=60000
Auth required Use --extra-headers or puppeteer script