rendering-performance

star 0

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

JayKim88 By JayKim88 schedule Updated 5/29/2026

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

  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.
Install via CLI
npx skills add https://github.com/JayKim88/claude-ai-engineering --skill rendering-performance
Repository Details
star Stars 0
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator