Lighthouse Local Perf Testing
Locally audit Core Web Vitals and Lighthouse scores against the dev server before every release / perf-related PR.
Quick run
task lighthouse # mobile, simulated throttling (most relevant for LCP/SEO)
task lighthouse:desktop # desktop preset (best-case numbers)
Reports land in .lighthouse/ (gitignored):
.lighthouse/mobile.report.html + mobile.report.json
.lighthouse/desktop.report.html + desktop.report.json
Pre-flight checklist
- Dev server must be up:
task dev (Docker, listens on localhost:3002 per memory).
- Chrome on host: Lighthouse runs headlessly via
npx --yes lighthouse against the host's Chrome — not in container. macOS users typically have it.
- First run downloads ~70MB of lighthouse bin into
~/.npm/_npx. Subsequent runs are fast.
Reading results — extract metrics from JSON
The HTML report is for humans. For Claude, parse the embedded JSON to surface key numbers concisely:
# mobile (clean JSON file)
node -e '
const r = JSON.parse(require("fs").readFileSync(".lighthouse/mobile.report.json", "utf8"));
const a = r.audits, c = r.categories;
console.log("=== Scores ==="); for (const [k,v] of Object.entries(c)) console.log(k.padEnd(15), Math.round((v.score??0)*100));
const fmt = (k,t=v=>v.toFixed(0)+" ms") => a[k]?.numericValue!==undefined ? t(a[k].numericValue) : "n/a";
console.log("\n=== Core Web Vitals ===");
console.log("LCP ", fmt("largest-contentful-paint"));
console.log("FCP ", fmt("first-contentful-paint"));
console.log("CLS ", fmt("cumulative-layout-shift", v=>v.toFixed(3)));
console.log("TBT ", fmt("total-blocking-time"));
console.log("Speed Index", fmt("speed-index"));
console.log("TTFB ", fmt("server-response-time"));
'
Dev vs prod expectations
The dev server runs Vite dev mode — un-minified ESM, large node_modules/.vite/deps/*.js shipments (lucide-react ~1MB, sentry ~1MB, react-dom ~1MB). So:
|
Dev (localhost) |
Prod (after deploy) |
| Performance (mobile) |
80–88 |
90+ |
| LCP (mobile) |
3–4s |
1.5–2s |
| TTFB |
300–500ms |
<100ms |
| Network total |
3–5MB |
500KB–1MB |
Rule of thumb: prod LCP ≈ dev LCP × 0.5. If dev LCP is good (< 4s) and there's no obvious render-blocking, prod will pass CWV.
When dev numbers aren't enough
For tighter prod-like measurement, run against a local prod build:
task build
docker compose exec app pnpm start & # or run on host via PORT=3002 pnpm start
task lighthouse
For real-world prod numbers post-deploy, use the metrifyr MCP (mcp__metrifyr__psi_analyze) — it hits the public URL with field/lab data and CrUX.
What to flag in a Lighthouse report
| signal |
what it means |
look for |
| LCP > 2.5s |
hero/above-fold image slow or render-blocked |
lcp-lazy-loaded, largest-contentful-paint-element, prioritize-lcp-image |
| CLS > 0.1 |
layout shifting |
layout-shifts, missing width/height on <img> |
| TBT > 200ms |
JS work blocking input |
bootup-time, mainthread-work-breakdown, unused-javascript |
| FCP > 2s |
first paint slow |
render-blocking-resources, font loading |
| SEO < 100 |
meta/markup issues |
meta-description, crawlable-anchors, hreflang |
Common PixelDen pitfalls (already known)
- Hero image as CSS
background-image — can never be optimal LCP. Always use <img> with srcset, width/height, fetchPriority="high". (Fixed in v0.2.7.)
- Public assets (
public/assets/**) are NOT fingerprinted by Vite. Combined with Cache-Control: immutable, max-age=31536000 in nginx, content changes need either a filename bump (-v2.webp) or a manual nginx purge — a known nginx static-cache trap.
game-canvas useEffect deps can re-mount Phaser unnecessarily. Verify before blaming network.
When NOT to use this skill
- The user is asking about prod / live-site perf → use
mcp__metrifyr__psi_analyze instead (PSI doesn't work against localhost).
- The user wants accessibility/a11y deep-dive → use the
chrome-devtools-mcp:a11y-debugging skill (Lighthouse a11y category is shallow).
- The user wants traces (call stacks, long tasks frame-by-frame) → use
mcp__chrome-devtools__performance_start_trace.
After running
Always summarize:
- Top-line scores (Performance / SEO / A11y / BP).
- Each Core Web Vital with rating (good / needs-improvement / poor).
- Top 1–3 opportunities (
audits with details.overallSavingsMs > 100).
- If LCP > 2.5s on mobile, identify the LCP element via
largest-contentful-paint-element audit.
- Don't forget: dev numbers ≠ prod numbers. State both expected.
1---2name: lighthouse3description: Run local Lighthouse perf audits on PixelDen dev server (localhost:3002). Use when the user asks about Lighthouse, LCP, FCP, CLS, INP, Core Web Vitals, performance score, perf regression, or wants to verify a perf fix locally before release. Triggers: 'lighthouse', 'LCP', 'CWV', 'perf test', 'pustit lighthouse', 'otestovat perf'.4---56# Lighthouse Local Perf Testing78Locally audit Core Web Vitals and Lighthouse scores against the dev server **before** every release / perf-related PR.910## Quick run1112```bash13task lighthouse # mobile, simulated throttling (most relevant for LCP/SEO)14task lighthouse:desktop # desktop preset (best-case numbers)15```1617Reports land in `.lighthouse/` (gitignored):1819- `.lighthouse/mobile.report.html` + `mobile.report.json`20- `.lighthouse/desktop.report.html` + `desktop.report.json`2122## Pre-flight checklist23241. **Dev server must be up:** `task dev` (Docker, listens on `localhost:3002` per memory).252. **Chrome on host:** Lighthouse runs headlessly via `npx --yes lighthouse` against the host's Chrome — not in container. macOS users typically have it.263. **First run downloads ~70MB** of lighthouse bin into `~/.npm/_npx`. Subsequent runs are fast.2728## Reading results — extract metrics from JSON2930The HTML report is for humans. For Claude, parse the embedded JSON to surface key numbers concisely:3132```bash33# mobile (clean JSON file)34node -e '35const r = JSON.parse(require("fs").readFileSync(".lighthouse/mobile.report.json", "utf8"));36const a = r.audits, c = r.categories;37console.log("=== Scores ==="); for (const [k,v] of Object.entries(c)) console.log(k.padEnd(15), Math.round((v.score??0)*100));38const fmt = (k,t=v=>v.toFixed(0)+" ms") => a[k]?.numericValue!==undefined ? t(a[k].numericValue) : "n/a";39console.log("\n=== Core Web Vitals ===");40console.log("LCP ", fmt("largest-contentful-paint"));41console.log("FCP ", fmt("first-contentful-paint"));42console.log("CLS ", fmt("cumulative-layout-shift", v=>v.toFixed(3)));43console.log("TBT ", fmt("total-blocking-time"));44console.log("Speed Index", fmt("speed-index"));45console.log("TTFB ", fmt("server-response-time"));46'47```4849## Dev vs prod expectations5051The dev server runs **Vite dev mode** — un-minified ESM, large `node_modules/.vite/deps/*.js` shipments (lucide-react ~1MB, sentry ~1MB, react-dom ~1MB). So:5253| | Dev (localhost) | Prod (after deploy) |54| -------------------- | --------------- | ------------------- |55| Performance (mobile) | 80–88 | 90+ |56| LCP (mobile) | 3–4s | 1.5–2s |57| TTFB | 300–500ms | <100ms |58| Network total | 3–5MB | 500KB–1MB |5960**Rule of thumb:** prod LCP ≈ dev LCP × 0.5. If dev LCP is good (< 4s) and there's no obvious render-blocking, prod will pass CWV.6162## When dev numbers aren't enough6364For tighter prod-like measurement, run against a local prod build:6566```bash67task build68docker compose exec app pnpm start & # or run on host via PORT=3002 pnpm start69task lighthouse70```7172For real-world prod numbers post-deploy, use the `metrifyr` MCP (`mcp__metrifyr__psi_analyze`) — it hits the public URL with field/lab data and CrUX.7374## What to flag in a Lighthouse report7576| signal | what it means | look for |77| ----------- | -------------------------------------------- | ----------------------------------------------------------------------------- |78| LCP > 2.5s | hero/above-fold image slow or render-blocked | `lcp-lazy-loaded`, `largest-contentful-paint-element`, `prioritize-lcp-image` |79| CLS > 0.1 | layout shifting | `layout-shifts`, missing `width`/`height` on `<img>` |80| TBT > 200ms | JS work blocking input | `bootup-time`, `mainthread-work-breakdown`, `unused-javascript` |81| FCP > 2s | first paint slow | `render-blocking-resources`, font loading |82| SEO < 100 | meta/markup issues | `meta-description`, `crawlable-anchors`, `hreflang` |8384## Common PixelDen pitfalls (already known)8586- **Hero image as CSS `background-image`** — can never be optimal LCP. Always use `<img>` with `srcset`, `width/height`, `fetchPriority="high"`. (Fixed in v0.2.7.)87- **Public assets (`public/assets/**`) are NOT fingerprinted by Vite.** Combined with `Cache-Control: immutable, max-age=31536000` in nginx, content changes need either a filename bump (`-v2.webp`) or a manual nginx purge — a known nginx static-cache trap.88- **`game-canvas` useEffect deps** can re-mount Phaser unnecessarily. Verify before blaming network.8990## When NOT to use this skill9192- The user is asking about prod / live-site perf → use `mcp__metrifyr__psi_analyze` instead (PSI doesn't work against `localhost`).93- The user wants accessibility/a11y deep-dive → use the `chrome-devtools-mcp:a11y-debugging` skill (Lighthouse a11y category is shallow).94- The user wants traces (call stacks, long tasks frame-by-frame) → use `mcp__chrome-devtools__performance_start_trace`.9596## After running9798Always summarize:991001. Top-line scores (Performance / SEO / A11y / BP).1012. Each Core Web Vital with rating (good / needs-improvement / poor).1023. Top 1–3 opportunities (`audits` with `details.overallSavingsMs > 100`).1034. If LCP > 2.5s on mobile, identify the LCP element via `largest-contentful-paint-element` audit.1045. **Don't forget**: dev numbers ≠ prod numbers. State both expected.