349 lines
15 KiB
Markdown
349 lines
15 KiB
Markdown
---
|
|
name: frontend-lighthouse
|
|
description: "Add a portable Lighthouse CI gate for production frontend builds with Core Web Vitals budgets, category floors, median runs, and CI artifacts."
|
|
category: frontend
|
|
risk: safe
|
|
source: community
|
|
source_repo: stareezy-1/frontend-architecture-skill
|
|
source_type: community
|
|
date_added: "2026-06-29"
|
|
author: stareezy-1
|
|
tags: [frontend, lighthouse, performance, core-web-vitals, ci]
|
|
tools: [lighthouse, node, github-actions]
|
|
license: "MIT"
|
|
license_source: "https://github.com/stareezy-1/frontend-architecture-skill/blob/main/LICENSE"
|
|
---
|
|
|
|
# Frontend Lighthouse (portable performance gate)
|
|
|
|
> 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.
|
|
|
|
## When to Use This Skill
|
|
|
|
- Use when adding a Lighthouse CI performance gate to a web app.
|
|
- Use when setting Core Web Vitals budgets for LCP, CLS, and TBT as the lab proxy for INP.
|
|
- Use when configuring category score floors for performance, SEO, accessibility, and best practices.
|
|
- Use when debugging flaky Lighthouse runs or making reports visible as CI artifacts.
|
|
|
|
---
|
|
|
|
## 0. The five core ideas
|
|
|
|
1. **One config, one source of truth.** All budgets and assertions live in a single `lighthouserc.cjs`. Named constants for each budget — no magic numbers buried in assertion objects.
|
|
2. **Gate the production build, never dev.** Lighthouse runs against `build` + `start` (the real, optimized output). Dev-server numbers are meaningless for a budget.
|
|
3. **Median-of-N kills flakiness.** Run 3+ times and assert on the median run, so per-run jitter (cold caches, CI noise) never red-flags a healthy build.
|
|
4. **Budgets encode Google's "good" thresholds.** LCP ≤ 2500 ms, INP ≤ 200 ms (gated via the TBT lab proxy), CLS ≤ 0.1 — the values that earn green scores, not "needs improvement".
|
|
5. **Blocking in CI, visible as artifacts.** A GitHub Action runs the gate on every PR touching the app and uploads the HTML/JSON reports so failures are debuggable.
|
|
|
|
---
|
|
|
|
## 1. Files this skill adds
|
|
|
|
```
|
|
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`.
|
|
|
|
```bash
|
|
pnpm add -D @lhci/cli # or npm i -D / yarn add -D
|
|
```
|
|
|
|
---
|
|
|
|
## 2. The config (`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:**
|
|
|
|
- Every budget is a named constant with a unit in its name (`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).
|
|
- Assert on TBT for INP in the lab; treat the experimental `interaction-to-next-paint` audit as a `warn`, not an `error` (it isn't present in every Lighthouse build).
|
|
- Keep `onlyCategories` to exactly what you gate — fewer audits, faster, less noise.
|
|
|
|
---
|
|
|
|
## 3. Choosing budget severity and thresholds
|
|
|
|
| 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.
|
|
|
|
---
|
|
|
|
## 4. The npm script
|
|
|
|
```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:
|
|
|
|
```bash
|
|
pnpm build && pnpm lhci
|
|
# desktop form factor:
|
|
LHCI_FORM_FACTOR=desktop pnpm build && LHCI_FORM_FACTOR=desktop pnpm lhci
|
|
```
|
|
|
|
---
|
|
|
|
## 5. The GitHub Actions workflow
|
|
|
|
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.
|
|
|
|
```yaml
|
|
name: 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:**
|
|
|
|
- Trigger on the app path **and** the workflow file so config changes are self-testing.
|
|
- `if: always()` on the upload step — you need the report most when the gate fails.
|
|
- Gate on the **production** build (`pnpm build` then the `start` server in `collect`).
|
|
- Match the CI Node/pnpm versions to the repo's pinned versions to avoid lockfile drift.
|
|
|
|
---
|
|
|
|
## 6. Framework adapters
|
|
|
|
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.
|
|
|
|
---
|
|
|
|
## 7. Debugging failing or flaky runs
|
|
|
|
- **Flaky LCP/TBT** → raise `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.
|
|
- **"server not ready" timeout** → fix `startServerReadyPattern` to match the framework's actual ready log, and raise `startServerReadyTimeout`.
|
|
- **Real regressions** → open the uploaded report artifact, read the failed audit's "Opportunities"/"Diagnostics", fix the cause (oversized image, render-blocking JS, layout shift from unsized media) — don't just bump the budget.
|
|
- **Desktop vs mobile divergence** → run both form factors; mobile is the stricter gate and should be the default.
|
|
|
|
---
|
|
|
|
## 8. Conventions checklist (enforce in review)
|
|
|
|
- [ ] All budgets are named constants with units and comments — no magic numbers in assertions.
|
|
- [ ] Gate runs against the **production** build, never the dev server.
|
|
- [ ] `aggregationMethod: "median-run"` with `numberOfRuns` ≥ 3.
|
|
- [ ] CWV budgets at Google "good" thresholds (LCP ≤ 2500, TBT ≤ 200, CLS ≤ 0.1).
|
|
- [ ] INP gated via TBT (`error`); experimental INP audit is `warn`.
|
|
- [ ] Category floors set as `error` (perf ≥ 0.9, SEO/a11y ≥ 0.95, best-practices ≥ 0.9).
|
|
- [ ] `onlyCategories` lists exactly the gated categories.
|
|
- [ ] CI triggers on the app path **and** the workflow file; reports upload with `if: always()`.
|
|
- [ ] Local `pnpm lhci` reproduces the CI run.
|
|
- [ ] Budgets are tightened over time, loosened only with a recorded reason.
|
|
|
|
---
|
|
|
|
## 9. How to apply this skill
|
|
|
|
**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).
|
|
|
|
---
|
|
|
|
## Publishing / installing this skill
|
|
|
|
This skill follows the Anthropic `SKILL.md` format and is portable across agents.
|
|
|
|
1. Keep it under `skills/frontend-lighthouse/SKILL.md` in a public GitHub repo.
|
|
2. Keep the frontmatter `name` and high-signal `description` — discovery indexes match against it.
|
|
3. Install with: `npx skills add <org>/<repo> --skill "frontend-lighthouse"`.
|
|
4. Non-`SKILL.md` agents can be pointed here from `AGENTS.md` / `CLAUDE.md`; Kiro can mirror it as a steering file.
|
|
|
|
## Limitations
|
|
|
|
- Lighthouse CI is a lab signal and does not replace field monitoring from real-user metrics.
|
|
- Budgets must be tuned to the actual app route, hosting platform, and device/network assumptions.
|
|
- A passing Lighthouse gate does not prove business-critical flows, visual correctness, or backend availability.
|