Install any skill in seconds. Free to start, no credit card required.
Get Started Free →A portable, framework-agnostic Lighthouse CI performance-gate system for any web frontend. Enforces Core Web Vitals budgets (LCP, INP via the TBT lab proxy, CLS) and category score floors (performance, SEO, accessibility, best-practices) as a blocking CI check on every pull request. Runs Lighthouse against the production build with median-of-N runs for stability, on mobile emulation by default with an opt-in desktop form factor. Ships a single `lighthouserc.cjs` config, an npm `lhci` script, and
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 82% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 190% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 141% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 317% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 294% | 0% |
> Portable skill — readable by Claude Code, OpenCode, Codex, Cursor, Windsurf, and others. > This skill describes a CI performance gate — a Lighthouse CI config plus a workflow — not a > component library or a visual style. It pairs with the frontend-seo and > frontend-architecture skills: SEO writes the metadata, Lighthouse proves it ships fast.
The goal: every pull request is blocked unless the production build meets explicit Core Web Vitals budgets and category score floors. Budgets live in one lighthouserc.cjs, runs are median-of-N so the gate doesn't flake, and the same config runs locally and in CI.
lighthouserc.cjs. Named constants for each budget — no magic numbers buried in assertion objects.build + start (the real, optimized output). Dev-server numbers are meaningless for a budget.apps/web/ (or your app root)
├── lighthouserc.cjs ← the gate: budgets + assertions + collect settings
├── package.json ← "lhci": "lhci autorun --config=./lighthouserc.cjs"
└── .github/workflows/lighthouse.yml ← PR-blocking CI job (build → start → lhci → upload)Plus a dev dependency: @lhci/cli.
bashpnpm add -D @lhci/cli # or npm i -D / yarn add -D
lighthouserc.cjs).cjs (CommonJS) so it loads without ESM/TS transpilation. Every budget is a named constant with a comment explaining the threshold — never a bare number inside an assertion.
js/** * Lighthouse CI configuration — Core Web Vitals budgets for the marketing surface. * * Enforces Google's mobile "good" CWV thresholds: * - Largest Contentful Paint (LCP) ≤ 2500 ms * - Cumulative Layout Shift (CLS) ≤ 0.1 * - Interaction to Next Paint (INP) ≤ 200 ms * * INP is a *field* metric with no direct lab audit, so in the lab we gate on * Total Blocking Time (TBT) — Lighthouse's recommended lab proxy — at the same * budget, and assert the experimental INP audit directly as a warning where the * build exposes it. * * Collection runs against the *production* server (build + start) on Lighthouse's * default mobile (Moto G4 / slow 4G) emulation. */ /** The fixed port the production server is started on for the audit. */ const PORT = 3100; const BASE_URL = `http://localhost:${PORT}`; /** Pages whose budgets are enforced in CI. */ const MARKETING_URLS = [`${BASE_URL}/`]; /** * Core Web Vitals budgets on mobile — Google's "good" thresholds. * These are the values that earn the best Lighthouse scores. */ const LCP_BUDGET_MS = 2500; // good const INP_BUDGET_MS = 200; // good (TBT lab proxy) const CLS_BUDGET = 0.1; // good module.exports = { ci: { collect: { // Build is run separately in CI; here we only serve the production output. startServerCommand: `pnpm start --port ${PORT}`, startServerReadyPattern: "Ready in", // framework's "server ready" log line startServerReadyTimeout: 120000, url: MARKETING_URLS, // Median of multiple runs keeps the gate stable against per-run jitter. numberOfRuns: 3, settings: { // Default mobile emulation; opt into desktop via env for a second run. preset: process.env.LHCI_FORM_FACTOR === "desktop" ? "desktop" : undefined, // Only gate the categories we care about; skip PWA category noise. onlyCategories: [ "performance", "seo", "accessibility", "best-practices", ], }, }, assert: { // Median across runs is the value compared against each budget. aggregationMethod: "median-run", assertions: { // --- Core Web Vitals budgets (the contract) --------------------- "largest-contentful-paint": [ "error", { maxNumericValue: LCP_BUDGET_MS }, ], "cumulative-layout-shift": ["error", { maxNumericValue: CLS_BUDGET }], "total-blocking-time": ["error", { maxNumericValue: INP_BUDGET_MS }], // Direct INP audit where the Lighthouse build exposes it (else ignored). "interaction-to-next-paint": [ "warn", { maxNumericValue: INP_BUDGET_MS }, ], // --- Category floors (target top Lighthouse scores) ------------- "categories:performance": ["error", { minScore: 0.9 }], "categories:seo": ["error", { minScore: 0.95 }], "categories:accessibility": ["error", { minScore: 0.95 }], "categories:best-practices": ["error", { minScore: 0.9 }], }, }, upload: { // Keep reports in the CI run's filesystem; no external LHCI server. target: "filesystem", outputDir: "./.lighthouseci", }, }, };
Hard rules:
LCP_BUDGET_MS) and a comment.aggregationMethod: "median-run" is non-negotiable — single-run gates flake constantly.numberOfRuns ≥ 3 (odd numbers give a clean median).interaction-to-next-paint audit as a warn, not an error (it isn't present in every Lighthouse build).onlyCategories to exactly what you gate — fewer audits, faster, less noise.| Audit / category | Severity | Threshold | Why | | --------------------------- | -------- | --------- | ----------------------------------------------------- | | largest-contentful-paint | error | ≤ 2500 ms | Google "good" LCP | | cumulative-layout-shift | error | ≤ 0.1 | Google "good" CLS | | total-blocking-time | error | ≤ 200 ms | INP lab proxy | | interaction-to-next-paint | warn | ≤ 200 ms | not in all builds; don't hard-fail on a missing audit | | categories:performance | error | ≥ 0.9 | top (green) band | | categories:seo | error | ≥ 0.95 | SEO is cheap to keep perfect | | categories:accessibility | error | ≥ 0.95 | a11y regressions must block | | categories:best-practices | error | ≥ 0.9 | green band |
Use error for contracts that must hold and warn for audits that are environment-dependent or aspirational. Start strict and only loosen with a recorded reason — a budget you keep raising to make CI pass is a budget that no longer protects anything.
jsonc// package.json { "scripts": { "lhci": "lhci autorun --config=./lighthouserc.cjs" } }
lhci autorun runs collect → assert → upload in sequence. Run it locally before pushing to reproduce exactly what CI does:
bashpnpm build && pnpm lhci # desktop form factor: LHCI_FORM_FACTOR=desktop pnpm build && LHCI_FORM_FACTOR=desktop pnpm lhci
Runs on PRs that touch the app or the workflow itself. Builds the production output, runs the gate, and always uploads the reports (even on failure) so a red check is debuggable.
yamlname: Lighthouse CWV on: pull_request: branches: [main] paths: - "apps/web/**" - ".github/workflows/lighthouse.yml" permissions: contents: read jobs: lighthouse: name: Lighthouse CWV (marketing pages) runs-on: ubuntu-latest defaults: run: working-directory: apps/web steps: - uses: actions/checkout@v4 - name: Setup pnpm uses: pnpm/action-setup@v4 # version comes from root package.json packageManager - name: Setup Node uses: actions/setup-node@v4 with: node-version: 22 cache: pnpm - name: Install dependencies working-directory: . run: pnpm install --frozen-lockfile - name: Build web app run: pnpm build # build + start the production server, run Lighthouse on mobile emulation, # fail the job if any budget in lighthouserc.cjs is exceeded. - name: Run Lighthouse CI run: pnpm lhci - name: Upload Lighthouse reports if: always() uses: actions/upload-artifact@v4 with: name: lighthouse-reports path: apps/web/.lighthouseci if-no-files-found: ignore
Hard rules:
if: always() on the upload step — you need the report most when the gate fails.pnpm build then the start server in collect).The config is framework-neutral except startServerCommand and startServerReadyPattern.
| Framework | startServerCommand | startServerReadyPattern | | ------------- | ----------------------------------------------------------------- | ------------------------------------------- | | Next.js | pnpm start --port 3100 (after next build) | "Ready in" | | Remix | pnpm start (serve the built app) | server's listening log line | | Astro | node ./dist/server/entry.mjs (SSR) or npx serve dist (static) | the adapter's ready line / serve's URL line | | SvelteKit | node build (node adapter) | "Listening on" | | Vite SPA | npx vite preview --port 3100 | "Local:" |
For purely static output you can skip the server and point collect.staticDistDir at the build folder instead of startServerCommand — Lighthouse serves it internally.
numberOfRuns (5), confirm median-run, and make sure nothing else is competing for CPU on the runner.interaction-to-next-paint errors → it should be warn, not error; the audit is missing in some Lighthouse versions.startServerReadyPattern to match the framework's actual ready log, and raise startServerReadyTimeout.aggregationMethod: "median-run" with numberOfRuns ≥ 3.error); experimental INP audit is warn.error (perf ≥ 0.9, SEO/a11y ≥ 0.95, best-practices ≥ 0.9).onlyCategories lists exactly the gated categories.if: always().pnpm lhci reproduces the CI run.Adding the gate to a project: install @lhci/cli, drop in lighthouserc.cjs with your URLs and startServerCommand, add the lhci script, and add the workflow. Run pnpm build && pnpm lhci locally to confirm it passes before opening a PR.
Adding a page to the gate: append its URL to MARKETING_URLS (or a second URL array). Each URL is audited independently against the same budgets.
Tuning budgets: change the named constant, not the assertion. Record why in the comment. Prefer fixing the regression over raising the budget.
Reviewing performance: run the checklist in §8. The highest-value catches are a gate that runs against the dev server (meaningless numbers) and single-run assertions (chronic flakiness).
This skill follows the Anthropic SKILL.md format and is portable across agents.
skills/frontend-lighthouse/SKILL.md in a public GitHub repo.name and high-signal description — discovery indexes match against it.npx skills add <org>/<repo> --skill "frontend-lighthouse".SKILL.md agents can be pointed here from AGENTS.md / CLAUDE.md; Kiro can mirror it as a steering file.Other measured skills in the registry, with their headline benchmark lift.