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
.claude/skills/stareezy-1-frontend-lighthouse/SKILL.md| 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.| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 13,918 | 9,982 | -28% | 1 | 1 | 0% | 3,536 | 6,453 | +82% | 0 | 0 | — |
case-02 | fail→pass | 9,479 | 10,588 | +12% | 1 | 1 | 0% | 2,290 | 6,637 | +190% | 0 | 0 | — |
case-03 | fail→pass | 8,953 | 4,947 | -45% | 1 | 1 | 0% | 1,954 | 4,711 | +141% | 0 | 0 | — |
case-04 | fail→fail | 5,029 | 4,645 | -8% | 1 | 1 | 0% | 1,094 | 4,721 | +332% | 0 | 0 | — |
case-05 | pass→pass | 4,159 | 3,200 | -23% | 1 | 1 | 0% | 854 | 4,428 | +419% | 0 | 0 | — |
case-06 | fail→fail | 4,392 | 4,812 | +10% | 1 | 1 | 0% | 957 | 4,682 | +389% | 0 | 0 | — |
case-07 | fail→fail | 7,237 | 5,863 | -19% | 1 | 1 | 0% | 1,398 | 4,962 | +255% | 0 | 0 | — |
case-08 | fail→pass | 5,091 | 6,341 | +25% | 1 | 1 | 0% | 1,190 | 4,959 | +317% | 0 | 0 | — |
case-09 | pass→pass | 5,263 | 3,165 | -40% | 1 | 1 | 0% | 1,036 | 4,288 | +314% | 0 | 0 | — |
case-10 | fail→pass | 5,007 | 3,492 | -30% | 1 | 1 | 0% | 1,109 | 4,366 | +294% | 0 | 0 | — |
case-11 | pass→pass | 8,774 | 3,232 | -63% | 1 | 1 | 0% | 1,911 | 4,358 | +128% | 0 | 0 | — |
case-12 | fail→pass | 5,109 | 4,446 | -13% | 1 | 1 | 0% | 760 | 4,562 | +500% | 0 | 0 | — |
case-13 | pass→pass | 6,647 | 3,099 | -53% | 1 | 1 | 0% | 1,449 | 4,320 | +198% | 0 | 0 | — |
case-14 | fail→fail | 5,445 | 3,585 | -34% | 1 | 1 | 0% | 1,199 | 4,471 | +273% | 0 | 0 | — |
case-15 | fail→pass | 5,105 | 3,843 | -25% | 1 | 1 | 0% | 1,127 | 4,327 | +284% | 0 | 0 | — |
case-16 | pass→pass | 2,462 | 1,707 | -31% | 1 | 1 | 0% | 497 | 4,039 | +713% | 0 | 0 | — |
case-17 | pass→pass | 7,432 | 4,542 | -39% | 1 | 1 | 0% | 1,698 | 4,716 | +178% | 0 | 0 | — |
case-18 | fail→pass | 9,331 | 2,306 | -75% | 1 | 1 | 0% | 1,505 | 4,105 | +173% | 0 | 0 | — |
case-19 | pass→pass | 10,742 | 4,634 | -57% | 1 | 1 | 0% | 2,157 | 4,563 | +112% | 0 | 0 | — |
case-20 | pass→pass | 9,267 | 10,318 | +11% | 1 | 1 | 0% | 2,196 | 6,032 | +175% | 0 | 0 | — |
case-21 | pass→pass | 11,383 | 8,252 | -28% | 1 | 1 | 0% | 2,350 | 5,333 | +127% | 0 | 0 | — |
case-22 | pass→pass | 7,368 | 8,041 | +9% | 1 | 1 | 0% | 1,677 | 5,477 | +227% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +36 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.