# Rendering Performance

> 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).

- Skill: `jaykim88/rendering-performance` (Agent Skill)
- Install (CLI): `npx skillmds@latest add jaykim88/rendering-performance`
- Raw SKILL.md: https://api.skillmd.com/api/skills/jaykim88/rendering-performance/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- License: MIT
- Author: JayKim88 (https://skillmd.com/u/jaykim88)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/jaykim88/rendering-performance

---


# 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

1. **Measure first — never optimize blind**
   - Install the `web-vitals` library 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

2. **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` (use `pagehide`), no `Cache-Control: no-store` on the document. Verify in DevTools → Application → Back/forward cache.
   - Likely-next navigation: consider Speculation Rules `prerender`.

3. **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 (see `bundle-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).

4. **CLS fixes (target ≤ 0.1)** — reserve space for anything that loads late: explicit `width`/`height` (or `aspect-ratio`) on media, skeletons sized to the *final* content, `font-display: optional`, slots for ads/embeds. Rule: never let late content push layout.

5. **Lists with 1000+ items → virtualize**
   - `react-window` or `TanStack Virtual`
   - Verify with Profiler: rerender cost should be constant regardless of list size

6. **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)
   - 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

7. **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.md` with sections:
  - `## Summary` — LCP / INP / CLS before & after (lab + field)
  - `## Critical/Major/Minor findings` per 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/image` with `priority` + `fetchpriority="high"` for LCP element
- Fonts: `next/font` with `display: 'swap'` or `'optional'`
- INP: React `startTransition` / `useDeferredValue`; `scheduler.yield()` for long tasks; `requestIdleCallback` for analytics; Web Workers
- Memoization: `React.memo` / `useCallback` / `useMemo` — only after Profiler proves cost
- Virtualization: `react-window` or `TanStack Virtual`
- Server work: Server Components / Server Actions

### Other stacks
- **Vue / Nuxt**: `<NuxtImg>` (with `format="webp"` and `priority`); `useFetch` for server work; `defineAsyncComponent` for code splitting; virtualization via `vue-virtual-scroller`
- **SvelteKit**: `enhanced:img` Vite plugin (Vite 5+); `+server.ts` for server work; Svelte 5 fine-grained reactivity reduces unnecessary updates; virtualization via `svelte-virtual-list`
- **Angular**: `<NgOptimizedImage>` with `priority`; `signal()` and `effect()` instead of `RxJS` for fine-grained updates; CDK Virtual Scroll
- **Universal**: `web-vitals` library 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 bottleneck
- `render-strategy-decision` — when the fix is moving work to the server
- `animation-quality` — when animation jank is causing INP regression

## Reference
- **Key insight encoded**: Diagnose-then-fix order matters — measure with the `web-vitals` library 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.

