<!-- skill: Web Performance | version: 1.0.0 | description: Core Web Vitals budgets, image/CDN/caching speed workflow | category: Web -->
## Instructions
- Budgets first: LCP ≤2.5s, CLS ≤0.1, INP ≤200ms (p75 mobile Moto G4/4G); set `performance-budget.json` (JS ≤170kB, CSS ≤50kB, images ≤800kB, 3P ≤3); CI fails Lighthouse <90.
- Measure lab + field: Lighthouse CI + WebPageTest (Dulles Moto G), CrUX field via `web-vitals` RUM (`onLCP/onCLS/onINP` → analytics); lab for regressions, field for truth.
- Images: AVIF primary + WebP fallback, `srcset widths 400/800/1200` + `sizes`, `fetchpriority="high"` hero + `loading="lazy"` below-fold, `width/height` to reserve space (CLS); `blurhash/LQIP` placeholder.
- Code-split: `dynamic(import())` routes + heavy charts, `React.lazy` islands, tree-shake icons (`import { X } from 'lucide-react/dist/...'` or per-icon), `bundle-analyzer` kill >30kB deps (moment→date-fns).
- CSS/JS delivery: critical CSS inline `<style>` above-fold, `defer` non-critical JS, `preconnect fonts.gstatic` + `preload` hero image/font `as="image/font" crossorigin`; no render-blocking 3P in `<head>`.
- Caching headers: `Cache-Control: public, max-age=31536000, immutable` hashed assets, `s-maxage=300, stale-while-revalidate=60` HTML/CDN, `ETag` revalidate APIs; version filenames `app.[hash].js`.
- CDN/edge: Cloudflare/Fastly in front, `cache-control s-maxage` + `Vary: Accept-Encoding`, Brotli-11 + HTTP/3, image CDN (`/cdn-cgi/image/width=800,format=auto`) or `next/image`/`NuxtImg` loader.
- Fonts: self-host `woff2` subset latin, `font-display: swap`, `preload` 1-2 weights only, `size-adjust` fallback to kill CLS; system stack for body if brand allows.
- INP: break 50ms tasks (`scheduler.yield`/`setTimeout` chunks), `content-visibility: auto` long lists, virtualize 1000+ rows, debounce inputs 150ms, move filters to worker; avoid `useEffect` waterfalls.
- LCP killers: hero never lazy, no carousel 3s delay, `fetchpriority` + `preload`, SSR hero HTML (no client fetch), edge-cache HTML; `elementtiming` attributions to find LCP node.
- Third-party: `facade` YouTube/maps (click-to-load), `partytown` analytics off-thread, `async/defer` chat widgets delayed 5s idle; audit `transfer size` per 3P, remove >50kB non-converting.
- Profiling workflow: Baseline (3 runs median) → trace (DevTools Performance + `?debug`) → fix top offender → re-run → guard (LHCI `assert` + RUM alert p95); record before/after in PR.
## Examples
- Blog LCP 4s→1.6s: AVIF hero `fetchpriority=high` + preload, critical CSS inline, remove 200kB slider lib, edge-cache HTML `s-maxage=300`, fonts subset + preload.
- Dashboard INP 600→120ms: virtualized table, debounced filters in worker, split chart chunk `dynamic()`, `content-visibility` cards, killed polling `every 1s` → `every 30s`.
- Ecom CLS 0.4→0.02: `width/height` all images, `size-adjust` fonts, reserved ad slots `min-height`, skeleton `loading.tsx` matching final layout.
## Workflows
1. Baseline: 3x Lighthouse mobile + WPT filmstrip + RUM `web-vitals` snapshot; record LCP/CLS/INP + budgets.
2. Image pass: AVIF/srcset/sizes, hero priority + preload, lazy rest, LQIP, width/height everywhere.
3. Code pass: split routes/islands, tree-shake, defer 3P + facades, critical CSS, preload fonts/hero.
4. Cache/CDN: immutable assets, SWR HTML, Brotli/H3, image CDN loader, `Vary` correct.
5. INP pass: chunk tasks, virtualize, debounce, worker filters, verify DevTools long-task zero.
6. Guard: LHCI assert in CI, RUM p75 alerts, PR before/after table, weekly CrUX review.
## Templates
- Images + fonts + preload:
```html
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="preload" as="image" fetchpriority="high" href="/hero-1200.avif"
  imagesrcset="/hero-400.avif 400w, /hero-800.avif 800w, /hero-1200.avif 1200w" imagesizes="100vw" />
<img src="/hero-800.avif" srcset="/hero-400.avif 400w, /hero-800.avif 800w, /hero-1200.avif 1200w"
  sizes="(max-width:800px) 100vw, 800px" width="1200" height="630" fetchpriority="high" alt="Hero" />
<img src="/card.avif" loading="lazy" width="600" height="400" alt="Card" />
<style>@font-face { font-family: Inter; src: url(/inter-latin.woff2) format(woff2); font-display: swap; }</style>
```
- RUM + budgets + lazy:
```js
import { onLCP, onCLS, onINP } from 'web-vitals';
for (const fn of [onLCP, onCLS, onINP]) fn((m) => navigator.sendBeacon('/rum', JSON.stringify(m)));
// lighthouserc.js
module.exports = { ci: { assert: { assertions: {
  'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
  'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
  'interactive': ['warn', { maxNumericValue: 3500 }] } } } };
// React split
const Chart = React.lazy(() => import('./HeavyChart'));
```
- CDN headers (Nginx/Next):
```
location /assets/ { add_header Cache-Control "public, max-age=31536000, immutable"; }
location / { add_header Cache-Control "public, s-maxage=300, stale-while-revalidate=60"; }
// next.config.js
async headers() { return [{ source: '/:all*(svg|jpg|avif)', headers: [
  { key: 'Cache-Control', value: 'public, max-age=31536000, immutable' }] }]; }
```
## Tools
- SARAH shell_run for `lighthouse --preset=desktop/mobile`, `lhci autorun`, `web-vitals` RUM, `bundlesize` gates.
- SARAH file_write for budgets/lighthouserc/headers-config; web_fetch for web.dev vitals + MDN caching.
## Advanced
- Edge HTML: middleware geo-personalize with `s-maxage` shared + `private` fragments via ESI/islands; `Speculation Rules` prerender next page, `103 Early Hints` preload hero before HTML done.
- Workers/streaming: React Server streaming `loading.tsx` shells, `Suspense` per widget so LCP not blocked by slow island; edge `ReadableStream` flush hero first, defer analytics.
- RUM ops: p75 by route/device/country, `attribution` build (LCP element, CLS source, INP phase), Sentry `trace` link long-tasks, alert Slack when p75 LCP >3s 15min.
## Documentation
- https://web.dev/articles/vitals
- https://web.dev/articles/lcp
- https://web.dev/articles/cls
- https://web.dev/articles/inp
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching
