Web Performance
Optimize loading speed, interactivity, and visual stability using Core Web Vitals and resource budgets.
When to Use
- Improving Lighthouse performance scores
- Diagnosing slow LCP, high INP, or layout shifts (CLS)
- Reducing JavaScript or CSS bundle size
- Optimizing images, fonts, or third-party scripts
- Auditing page load performance across frameworks
Don’t use for:
- React component re-render optimization (use
react) - Next.js image/font components (use
next) - Accessibility audits (use
a11y) - SEO meta tags or structured data (use
web-seo)
Critical Patterns
✅ REQUIRED: Core Web Vitals Thresholds
LCP (Largest Contentful Paint) Good: ≤ 2.5s Poor: > 4.0s
INP (Interaction to Next Paint) Good: ≤ 200ms Poor: > 500ms
CLS (Cumulative Layout Shift) Good: ≤ 0.1 Poor: > 0.25
Measure with: Chrome DevTools Performance tab, Lighthouse, Web Vitals extension, or web-vitals npm package.
✅ REQUIRED: Resource Budgets
JavaScript (compressed): < 300 KB
CSS (compressed): < 100 KB
Total page weight: < 1.5 MB
Enforce with bundler size limits (Vite build.rollupOptions, Webpack performance.maxAssetSize).
✅ REQUIRED: Image Optimization
<!-- ✅ CORRECT: Modern format, explicit size, lazy loading -->
<img src="hero.webp" width="800" height="600"
alt="Hero image" loading="lazy" fetchpriority="high" />
<!-- ❌ WRONG: No dimensions (causes CLS), no lazy loading -->
<img src="photo.jpg" alt="Photo" />
Rules: Always specify width/height to prevent CLS. Use loading="lazy" for below-fold images. Use fetchpriority="high" on the LCP image only.
✅ REQUIRED: Font Loading — Eliminate CLS
<!-- ✅ Preload critical font + swap -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preload" href="/fonts/inter.woff2" as="font"
type="font/woff2" crossorigin />
/* ✅ font-display: swap prevents invisible text, reduces CLS */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter.woff2') format('woff2');
font-display: swap;
}
✅ REQUIRED: Script Loading
<!-- ✅ Async for independent scripts -->
<script src="analytics.js" async></script>
<!-- ✅ Defer for scripts that need DOM -->
<script src="app.js" defer></script>
<!-- ❌ WRONG: Blocking render -->
<script src="app.js"></script>
❌ NEVER: Render-Blocking Resources
<!-- ❌ WRONG: Blocks rendering -->
<link rel="stylesheet" href="non-critical.css" />
<!-- ✅ CORRECT: Inline critical CSS, async-load the rest -->
<style>/* critical above-fold styles */</style>
<link rel="stylesheet" href="styles.css" media="print"
onload="this.media='all'" />
Decision Tree
LCP > 2.5s?
→ Is LCP element an image? → Optimize image size, add fetchpriority="high", use WebP/AVIF
→ Is LCP element text? → Check font loading (font-display: swap + preload)
→ Slow server response? → Add SSR/SSG or caching layer
→ See references/core-web-vitals.md for full LCP diagnosis
INP > 200ms?
→ Long JS tasks on interaction? → Split tasks with scheduler.yield() or setTimeout
→ Heavy input handler? → Debounce/throttle, move work to Web Worker
→ See references/core-web-vitals.md for full INP diagnosis
CLS > 0.1?
→ Images without dimensions? → Add explicit width/height attributes
→ Font swap causing jump? → Preload font + font-display: swap or optional
→ Dynamic content inserted above fold? → Reserve space with min-height
→ See references/core-web-vitals.md for full CLS diagnosis
Bundle too large (JS > 300KB)?
→ Identify with source-map-explorer or bundler analyzer
→ Lazy-load routes and heavy components (dynamic import)
→ Tree-shake unused exports, audit dependencies
→ See references/resource-optimization.md
Images unoptimized?
→ Convert to WebP/AVIF, add responsive srcset, enable lazy loading
→ See references/image-video.md
Font causing layout shift?
→ Add font-display: swap, preload critical fonts, use size-adjust
→ See references/font-loading.md
Framework-specific?
→ React: re-render optimization → react skill
→ Next.js: next/image, next/font, PPR → next skill
→ Astro: output mode, islands → astro skill
Example
Diagnosing and fixing poor LCP on an e-commerce product page.
<!-- Before: LCP image unoptimized, no priority hint -->
<img src="product.jpg" class="hero" alt="Running shoes" />
<!-- After: WebP format, explicit dimensions, LCP hint, lazy for rest -->
<img
src="product.webp"
width="800" height="600"
alt="Running shoes"
fetchpriority="high"
decoding="async"
/>
<img src="thumbnail.webp" width="200" height="150"
alt="Side view" loading="lazy" />
<!-- Preload font to prevent invisible text during load -->
<link rel="preload" href="/fonts/inter-regular.woff2"
as="font" type="font/woff2" crossorigin />
// Measure LCP with web-vitals library
import { onLCP, onINP, onCLS } from 'web-vitals';
onLCP(({ value }) => console.log('LCP:', value));
onINP(({ value }) => console.log('INP:', value));
onCLS(({ value }) => console.log('CLS:', value));
Result: LCP drops from 4.2s → 1.8s after image format change + fetchpriority hint.
Edge Cases
Third-party scripts: Ad scripts, chat widgets, and analytics are common INP offenders. Load with async, defer initialization, or use a Facade pattern (lazy-load on first interaction).
Variable fonts: A single variable font file replaces multiple weight files, reducing requests. Use font-display: optional if layout shift on first load is unacceptable.
Responsive images: Use srcset + sizes for art direction. Without sizes, browser downloads the wrong size.
<img srcset="img-400.webp 400w, img-800.webp 800w"
sizes="(max-width: 600px) 400px, 800px"
src="img-800.webp" alt="..." />
INP vs FID: FID (First Input Delay) was replaced by INP in March 2024. INP measures all interactions throughout page lifetime, not just the first.
Resources
- Core Web Vitals — LCP, INP, CLS measurement and diagnosis
- Resource Optimization — JS/CSS budgets, bundling, tree-shaking
- Image & Video — Formats, responsive images, lazy loading
- Font Loading — font-display strategies, preloading, CLS elimination
See references/README.md for navigation.