When to Use
- Use when: Lighthouse score < 70 on mobile or desktop
- Use when: LCP > 2.5s, INP > 200ms, or CLS > 0.1
- Use when: bundle size review before shipping new dependencies
- Use when: images are unoptimized or causing layout shift
- Do NOT use for: database query optimization — that's a backend concern
- Do NOT use for: React rendering performance (memo, useMemo) — that's a separate profiling task
Core Web Vitals — Targets and Causes
LCP — Largest Contentful Paint (target: < 2.5s)
What causes LCP to fail:
| Cause |
Fix |
| Render-blocking CSS/JS |
<link rel="preload"> critical CSS; defer non-critical JS |
| Unoptimized hero image |
WebP/AVIF + srcset + fetchpriority="high" on LCP image |
| Slow server response (TTFB > 600ms) |
CDN, edge caching, server-side rendering |
| Lazy-loaded LCP element |
Remove loading="lazy" from the above-fold image |
| Web font blocking render |
font-display: swap + preload critical font file |
INP — Interaction to Next Paint (target: < 200ms)
Replaced FID as Core Web Vital in March 2024 — measure this, not FID.
What causes INP to fail:
| Cause |
Fix |
| Long event handlers (> 50ms) |
Break into smaller tasks with scheduler.yield() / setTimeout(0) |
| Heavy JS on main thread |
Move to Web Worker or defer until after interaction |
| Forced synchronous layout |
Batch DOM reads/writes; avoid read-then-write patterns |
| Third-party scripts blocking |
Load third-party scripts with async/defer, or lazy-load |
CLS — Cumulative Layout Shift (target: < 0.1)
What causes CLS to fail:
| Cause |
Fix |
| Images without dimensions |
Always set width + height on <img>, or use aspect-ratio in CSS |
| Web fonts causing FOUT |
font-display: optional or reserve space with size-adjust |
| Dynamic content inserted above existing |
Reserve space with min-height before content loads |
| Ads / embeds without dimensions |
Fixed-size containers for ad slots |
Bundle Optimization
Code splitting
// Route-level splitting (React)
const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));
// Wrap in Suspense with skeleton fallback
<Suspense fallback={<DashboardSkeleton />}>
<Dashboard />
</Suspense>
Webpack / Vite vendor chunk split
// vite.config.js
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
charts: ['recharts'],
}
}
}
}
Tree shaking — what breaks it
import * as X from 'lib' — prevents tree shaking; use named imports
- CommonJS
require() — not tree-shakeable; prefer ESM
- Side-effectful imports — mark packages as
"sideEffects": false in package.json
Bundle analysis
# Vite
npx vite-bundle-analyzer
# Webpack
npx webpack-bundle-analyzer stats.json
Image Optimization
<!-- Modern format with fallback -->
<picture>
<source srcset="hero.avif" type="image/avif">
<source srcset="hero.webp" type="image/webp">
<img src="hero.jpg"
width="1200" height="630"
alt="..."
fetchpriority="high" <!-- LCP image only -->
decoding="async">
</picture>
<!-- Below-fold images: lazy load -->
<img src="card.webp" loading="lazy" decoding="async" width="400" height="300" alt="...">
Rules:
- LCP image: never
loading="lazy", always fetchpriority="high"
- Every
<img>: always set width + height to prevent CLS
- Prefer AVIF > WebP > JPEG for photos; SVG for icons/illustrations
- Responsive
srcset: provide 1×, 1.5×, 2× versions
Caching Strategies
HTTP cache headers
# Immutable assets (hashed filenames) — cache forever
Cache-Control: public, max-age=31536000, immutable
# HTML — always revalidate
Cache-Control: no-cache
# API responses — short cache
Cache-Control: public, max-age=60, stale-while-revalidate=300
Service Worker (cache-first for assets, network-first for API)
// Cache-first for static assets
self.addEventListener('fetch', (e) => {
if (e.request.destination === 'image' || e.request.url.includes('/assets/')) {
e.respondWith(caches.match(e.request).then(r => r || fetch(e.request)));
}
});
Resource Hints
<!-- DNS + TLS early for external origins -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://cdn.example.com" crossorigin>
<!-- Preload critical resources (LCP image, key font) -->
<link rel="preload" href="/fonts/inter-400.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/hero.avif" as="image">
<!-- Prefetch next-page resources (low priority, idle time) -->
<link rel="prefetch" href="/dashboard-chunk.js">
Optimization Priority
| Effort |
Actions |
| Quick (1–2 days) |
Enable gzip/Brotli, defer non-critical JS, add image width/height, font-display: swap |
| Medium (1–2 weeks) |
Route-level code splitting, service worker, WebP/AVIF conversion, preconnect hints |
| Long-term (1–3 months) |
CDN, edge rendering, architecture refactor, performance budget enforcement |
Measurement
# Lighthouse CLI
npx lighthouse https://yoursite.com --output=html --view
# Field data (web-vitals library in production)
import {onLCP, onINP, onCLS} from 'web-vitals';
onLCP(console.log);
onINP(console.log);
onCLS(console.log);
Always test on throttled mobile (Lighthouse preset: Moto G4, slow 4G).
Desktop scores mislead — real users are on slower devices.
Anti-Fake-Pass Rules
Before claiming performance is optimized, you MUST show:
Reference: gates/anti-fake-pass-gate.md
1---2name: web-performance3description: Audit and optimize web performance — Core Web Vitals (LCP, INP, CLS), bundle splitting, lazy loading, image optimization, caching strategies, and resource hints. Use when asked to "improve performance", "fix LCP/CLS/INP", "optimize bundle", "reduce load time", "lazy load images", "add caching", or before marking any public-facing page as production-ready. Do NOT use for server-side database performance — this covers frontend and network-layer only.4license: MIT © 2025 Claude Skills Maintainers5---6
7<!-- Adapted from secondsky/claude-skills (MIT) — web-performance-optimization and
8 web-performance-audit skills. Core Web Vitals targets, audit process, optimization
9 timeline. YAMTAM structure, per-metric diagnosis tables, and Anti-Fake-Pass are original. -->
10
11## When to Use
12
13- Use when: Lighthouse score < 70 on mobile or desktop
14- Use when: LCP > 2.5s, INP > 200ms, or CLS > 0.1
15- Use when: bundle size review before shipping new dependencies
16- Use when: images are unoptimized or causing layout shift
17- Do NOT use for: database query optimization — that's a backend concern
18- Do NOT use for: React rendering performance (memo, useMemo) — that's a separate profiling task
19
20---
21
22## Core Web Vitals — Targets and Causes
23
24### LCP — Largest Contentful Paint (target: < 2.5s)
25
26What causes LCP to fail:
27| Cause | Fix |
28|---|---|
29| Render-blocking CSS/JS | `<link rel="preload">` critical CSS; defer non-critical JS |
30| Unoptimized hero image | WebP/AVIF + `srcset` + `fetchpriority="high"` on LCP image |
31| Slow server response (TTFB > 600ms) | CDN, edge caching, server-side rendering |
32| Lazy-loaded LCP element | Remove `loading="lazy"` from the above-fold image |
33| Web font blocking render | `font-display: swap` + preload critical font file |
34
35### INP — Interaction to Next Paint (target: < 200ms)
36*Replaced FID as Core Web Vital in March 2024 — measure this, not FID.*
37
38What causes INP to fail:
39| Cause | Fix |
40|---|---|
41| Long event handlers (> 50ms) | Break into smaller tasks with `scheduler.yield()` / `setTimeout(0)` |
42| Heavy JS on main thread | Move to Web Worker or defer until after interaction |
43| Forced synchronous layout | Batch DOM reads/writes; avoid read-then-write patterns |
44| Third-party scripts blocking | Load third-party scripts with `async`/`defer`, or lazy-load |
45
46### CLS — Cumulative Layout Shift (target: < 0.1)
47
48What causes CLS to fail:
49| Cause | Fix |
50|---|---|
51| Images without dimensions | Always set `width` + `height` on `<img>`, or use `aspect-ratio` in CSS |
52| Web fonts causing FOUT | `font-display: optional` or reserve space with `size-adjust` |
53| Dynamic content inserted above existing | Reserve space with `min-height` before content loads |
54| Ads / embeds without dimensions | Fixed-size containers for ad slots |
55
56---
57
58## Bundle Optimization
59
60### Code splitting
61```js
62// Route-level splitting (React)
63const Dashboard = lazy(() => import('./Dashboard'));
64const Settings = lazy(() => import('./Settings'));
65
66// Wrap in Suspense with skeleton fallback
67<Suspense fallback={<DashboardSkeleton />}>
68 <Dashboard />
69</Suspense>
70```
71
72### Webpack / Vite vendor chunk split
73```js
74// vite.config.js
75build: {
76 rollupOptions: {
77 output: {
78 manualChunks: {
79 vendor: ['react', 'react-dom'],
80 charts: ['recharts'],
81 }
82 }
83 }
84}
85```
86
87### Tree shaking — what breaks it
88- `import * as X from 'lib'` — prevents tree shaking; use named imports
89- CommonJS `require()` — not tree-shakeable; prefer ESM
90- Side-effectful imports — mark packages as `"sideEffects": false` in package.json
91
92### Bundle analysis
93```bash
94# Vite
95npx vite-bundle-analyzer
96# Webpack
97npx webpack-bundle-analyzer stats.json
98```
99
100---
101
102## Image Optimization
103
104```html
105<!-- Modern format with fallback -->
106<picture>
107 <source srcset="hero.avif" type="image/avif">
108 <source srcset="hero.webp" type="image/webp">
109 <img src="hero.jpg"
110 width="1200" height="630"
111 alt="..."
112 fetchpriority="high" <!-- LCP image only -->
113 decoding="async">
114</picture>
115
116<!-- Below-fold images: lazy load -->
117<img src="card.webp" loading="lazy" decoding="async" width="400" height="300" alt="...">
118```
119
120Rules:
121- **LCP image**: never `loading="lazy"`, always `fetchpriority="high"`
122- **Every `<img>`**: always set `width` + `height` to prevent CLS
123- Prefer AVIF > WebP > JPEG for photos; SVG for icons/illustrations
124- Responsive `srcset`: provide 1×, 1.5×, 2× versions
125
126---
127
128## Caching Strategies
129
130### HTTP cache headers
131```
132# Immutable assets (hashed filenames) — cache forever
133Cache-Control: public, max-age=31536000, immutable
134
135# HTML — always revalidate
136Cache-Control: no-cache
137
138# API responses — short cache
139Cache-Control: public, max-age=60, stale-while-revalidate=300
140```
141
142### Service Worker (cache-first for assets, network-first for API)
143```js
144// Cache-first for static assets
145self.addEventListener('fetch', (e) => {
146 if (e.request.destination === 'image' || e.request.url.includes('/assets/')) {
147 e.respondWith(caches.match(e.request).then(r => r || fetch(e.request)));
148 }
149});
150```
151
152---
153
154## Resource Hints
155
156```html
157<!-- DNS + TLS early for external origins -->
158<link rel="preconnect" href="https://fonts.googleapis.com">
159<link rel="preconnect" href="https://cdn.example.com" crossorigin>
160
161<!-- Preload critical resources (LCP image, key font) -->
162<link rel="preload" href="/fonts/inter-400.woff2" as="font" type="font/woff2" crossorigin>
163<link rel="preload" href="/hero.avif" as="image">
164
165<!-- Prefetch next-page resources (low priority, idle time) -->
166<link rel="prefetch" href="/dashboard-chunk.js">
167```
168
169---
170
171## Optimization Priority
172
173| Effort | Actions |
174|---|---|
175| Quick (1–2 days) | Enable gzip/Brotli, defer non-critical JS, add image `width`/`height`, `font-display: swap` |
176| Medium (1–2 weeks) | Route-level code splitting, service worker, WebP/AVIF conversion, preconnect hints |
177| Long-term (1–3 months) | CDN, edge rendering, architecture refactor, performance budget enforcement |
178
179---
180
181## Measurement
182
183```bash
184# Lighthouse CLI
185npx lighthouse https://yoursite.com --output=html --view
186
187# Field data (web-vitals library in production)
188import {onLCP, onINP, onCLS} from 'web-vitals';
189onLCP(console.log);
190onINP(console.log);
191onCLS(console.log);
192```
193
194**Always test on throttled mobile** (Lighthouse preset: Moto G4, slow 4G).
195Desktop scores mislead — real users are on slower devices.
196
197---
198
199## Anti-Fake-Pass Rules
200
201Before claiming performance is optimized, you MUST show:
202- [ ] LCP, INP, CLS — all three measured (Lighthouse score or field data)
203- [ ] Every `<img>` has `width` + `height` set (CLS prevention)
204- [ ] LCP image: no `loading="lazy"`, has `fetchpriority="high"`
205- [ ] Bundle: at least route-level code splitting present
206- [ ] Caching headers defined for static assets and HTML separately
207- [ ] Tested on throttled mobile, not just desktop
208
209Reference: `gates/anti-fake-pass-gate.md`