name: optimizing-web-performance description: Optimizes web page loading and interactivity against Core Web Vitals budgets — LCP, INP, CLS, JS weight — via image/font optimization, code-splitting, and script loading. Use when the user asks to make a page or site faster, fix Core Web Vitals, improve LCP, INP, CLS, or Lighthouse/PageSpeed scores, reduce bundle size, optimize images or fonts, or diagnose slow page loads. Do not use for animation smoothness or jank during interactions (crafting-ui-animations) or for backend/API latency.
Optimizing Web Performance
When to use / when NOT to use
- Use for: page-load and interactivity performance — Core Web Vitals, bundle size, images, fonts, script loading, hydration cost.
- Do NOT use for: animation frame-rate work (crafting-ui-animations), server/database latency, or React render architecture (building-react-components).
Budgets (2026 targets, p75 real-user)
| Metric | Budget |
|---|---|
| LCP (Largest Contentful Paint) | ≤ 2.5 s |
| INP (Interaction to Next Paint) | ≤ 200 ms |
| CLS (Cumulative Layout Shift) | ≤ 0.1 |
| JavaScript, gzipped, interactive page | ≤ 400 KB |
Core rules
-
Measure before optimizing. Run Lighthouse (lab) and check real-user data (CrUX / RUM) first; fix the worst failing metric, not the easiest one.
- ❌ "minify everything" before knowing whether LCP or INP is the problem
-
The LCP element loads first, eagerly. Preload the hero image, serve it in AVIF/WebP, and never lazy-load it.
- ✅
<link rel="preload" as="image" href="hero.avif">+<img fetchpriority="high" …> - ❌
<img loading="lazy">on the hero
- ✅
-
Every image ships sized, modern, and responsive.
width/heightattributes (prevents CLS),srcset/sizesfor viewports,loading="lazy"below the fold only. -
Fonts: self-host, subset,
font-display: swap, preload the one used above the fold. Two families maximum; variable font when more than two weights are needed. -
Ship less JavaScript. Code-split by route,
import()heavy widgets on interaction, prefer server rendering with minimal hydration; audit with a bundle analyzer before adding any dependency over ~10 KB gz.- ✅
const Chart = lazy(() => import('./Chart'))mounted when scrolled into view - ❌ charting + date + animation libraries in the entry bundle
- ✅
-
Nothing render-blocking, no third-party scripts in the critical path.
deferall scripts; load analytics/chat/ads after load or on idle; inline only the critical CSS. -
Reserve space for everything that arrives late. Ads, embeds, banners, and skeletons get fixed dimensions or
aspect-ratioso nothing shifts (CLS). -
Long tasks break INP — chunk them. Split main-thread work over 50 ms with
scheduler.yield()/setTimeout, debounce input handlers, move pure computation to a Web Worker.
Workflow
- Baseline: run Lighthouse on the target page (mobile, throttled) and record LCP/INP/CLS/JS-weight against the budget table.
- Identify the single worst offender per failing metric (LCP element, longest task, largest shift source, biggest bundle chunk).
- Apply the matching rule (2–8) to that offender only.
- Re-run Lighthouse; confirm the metric moved and no other metric regressed.
- Repeat 2–4 until all budgets pass, then verify with real-user data after deploy.
Edge cases & failure modes
- Lab passes, field fails → trust field data; test on a low-end device profile and slow 4G; check geographic latency to origin (CDN).
- LCP element is text → the font is the bottleneck: preload it, subset it, check
font-display. - Third-party script is required by the business → load it after
loadevent via a facade (static placeholder that loads the real widget on interaction); it cannot live in the critical path. - Framework hydration dominates JS cost → move non-interactive parts to server components/static rendering; hydrate islands only.
- No RUM available → use CrUX (public, origin-level) as the field proxy; add
web-vitals(npm) reporting when possible.
References
Copy-paste snippets and gotchas: see references/patterns.md