name: rendering-performance description: Diagnose and fix Core Web Vitals (LCP ≤ 2.5s, INP ≤ 200ms, CLS ≤ 0.1) with measurement-first methodology. Use when Lighthouse drops, CWV field data fails, users report slowness, or before shipping. Not for when JS payload is the bottleneck (use bundle-optimization) or moving work to the server (use render-strategy-decision). license: MIT
Rendering Performance
Purpose
Achieve and sustain Core Web Vitals targets through measure-then-fix discipline. Never guess at optimization — measure first, fix specifically, verify the metric moved.
Universal — Core Web Vitals (LCP / INP / CLS) are browser-level metrics, identical for every framework. The diagnose-then-fix order and fix categories (image priority, font display, JS payload reduction, virtualization) apply everywhere; only tool names differ.
Procedure
Measure first — never optimize blind
- Install the
web-vitalslibrary and report LCP / INP / CLS to analytics - Open PageSpeed Insights for the route → check FIELD data (real users), not just LAB
- Use the framework's DevTools profiler to identify wasteful rerenders
- Install the
LCP fixes (target ≤ 2.5s)
- Identify the LCP element, then apply the standard levers (syntax in Implementation): prioritize + modern-format the LCP image, preload critical fonts (
display: swap/optional), inline above-fold CSS, server-render the element (not client-only). - Trap — LCP has a server (TTFB) component: if TTFB dominates the LCP breakdown, none of the client levers above will move it →
render-strategy-decision/api-caching-optimization. - bfcache (cheap, commonly missed): preserve eligibility for near-instant back/forward nav — avoid
unload(usepagehide), noCache-Control: no-storeon the document. Verify in DevTools → Application → Back/forward cache. - Likely-next navigation: consider Speculation Rules
prerender.
- Identify the LCP element, then apply the standard levers (syntax in Implementation): prioritize + modern-format the LCP image, preload critical fonts (
INP fixes (target ≤ 200ms)
- Diagnose first:
web-vitals/attribution+ LoAF (Long Animation Frames) pinpoint the exact script/phase; React DevTools Profiler → find Long Tasks (50ms+). - Standard levers (syntax in Implementation): defer non-urgent updates (
startTransition/useDeferredValue), break long tasks (scheduler.yield()), push work to idle (requestIdleCallback) / a Web Worker / the server (Server Components), and cut initial JS (seebundle-optimization). - Common real-world INP killers: a controlled input re-rendering the whole form on every keystroke (isolate the field /
useDeferredValue); oversized Client Components hydrating (shrink the'use client'boundary).
- Diagnose first:
CLS fixes (target ≤ 0.1) — reserve space for anything that loads late: explicit
width/height(oraspect-ratio) on media, skeletons sized to the final content,font-display: optional, slots for ads/embeds. Rule: never let late content push layout.Lists with 1000+ items → virtualize
react-windoworTanStack Virtual- Verify with Profiler: rerender cost should be constant regardless of list size
Cut unnecessary work per update (cause first, memoize last)
- When the Profiler flags a hot unit, reduce the work it redoes each cycle before caching — usually cheaper than memoization:
- Derive, don't store — recompute cheap values instead of holding + syncing them (see
state-management-decisions) - Stabilize references crossing the update boundary — a fresh object/array/function each cycle defeats the framework's change-detection / skip-render optimizations
- Don't re-render a wide subtree for a narrow change — keep state close to where it's used; put broad shared state behind selectors
- Virtualize large collections (step 5) so per-update work stays constant, not O(n)
- Derive, don't store — recompute cheap values instead of holding + syncing them (see
- Only when the work can't be removed: memoize the expensive computation/component, then verify the hot path actually shrank
- Memoization is itself a cost (comparison, memory, complexity) — never apply it prophylactically
- See Implementation for the per-framework primitives
- When the Profiler flags a hot unit, reduce the work it redoes each cycle before caching — usually cheaper than memoization:
Verify after each fix (validation loop)
- Re-run Lighthouse; if Performance score < 90, identify the worst-affecting metric in the breakdown and apply the corresponding fix from steps 2-4; re-run until ≥ 90
- Check field data after a week (lab can lie; field is truth) — if any CWV metric fails at p75, return to step 2/3/4
Severity tiers
| Tier | Examples | Action SLA |
|---|---|---|
| Critical | LCP > 4s, INP > 500ms, CLS > 0.25 (p75 field data) | Block release; fix immediately |
| Major | LCP 2.5-4s, INP 200-500ms, CLS 0.1-0.25 | Fix this sprint |
| Minor | All CWV targets met; room to improve (e.g., 2.0s LCP could be 1.5s) | Schedule within 2 sprints |
Completion Criteria
- LCP ≤ 2.5s (field data, p75)
- INP ≤ 200ms (field data, p75)
- CLS ≤ 0.1 (field data, p75)
- Lighthouse Performance ≥ 90 (lab data)
- No prophylactic memoization without Profiler evidence
- All Critical findings fixed; all Major findings scheduled
Output
- Report:
docs/perf-audit-YYYY-MM-DD.mdwith sections:## Summary— LCP / INP / CLS before & after (lab + field)## Critical/Major/Minor findingsper severity tier## Fixes applied— per fix: metric affected, change, before/after measurement
- Code changes: one commit per fix; commit format
perf(<metric>): <description>(e.g.,perf(LCP): add fetchpriority to hero image) - Lighthouse reports: before/after JSON saved at
docs/lighthouse-YYYY-MM-DD-{before,after}.json
Implementation
React + Next.js (default)
- Images:
next/imagewithpriority+fetchpriority="high"for LCP element - Fonts:
next/fontwithdisplay: 'swap'or'optional' - INP: React
startTransition/useDeferredValue;scheduler.yield()for long tasks;requestIdleCallbackfor analytics; Web Workers - Memoization:
React.memo/useCallback/useMemo— only after Profiler proves cost - Virtualization:
react-windoworTanStack Virtual - Server work: Server Components / Server Actions
Other stacks
- Vue / Nuxt:
<NuxtImg>(withformat="webp"andpriority);useFetchfor server work;defineAsyncComponentfor code splitting; virtualization viavue-virtual-scroller - SvelteKit:
enhanced:imgVite plugin (Vite 5+);+server.tsfor server work; Svelte 5 fine-grained reactivity reduces unnecessary updates; virtualization viasvelte-virtual-list - Angular:
<NgOptimizedImage>withpriority;signal()andeffect()instead ofRxJSfor fine-grained updates; CDK Virtual Scroll - Universal:
web-vitalslibrary is framework-agnostic; Lighthouse / PageSpeed Insights audits any URL; LoAF API works in any browser
Related skills
bundle-optimization— when JS payload is the LCP/INP bottleneckrender-strategy-decision— when the fix is moving work to the serveranimation-quality— when animation jank is causing INP regression
Reference
- Key insight encoded: Diagnose-then-fix order matters — measure with the
web-vitalslibrary in the field before optimizing, then apply LCP / INP / CLS fixes as separate playbooks. Field data > lab data: lab Lighthouse can mislead on real-user variability, especially INP which depends on actual interaction patterns.