Web Performance — Review Skill
The performance review lens for web UI: what the page costs to load and to interact with, measured against a budget. Framework-neutral — the metrics and diagnoses are the same for any stack; examples are HTML plus a little TSX for where a framework changes the fix.
Builds on.
react,angular, orvuefor the framework rendering and hydration API (their rendering and SSR groups), andarchitecture-and-designfor data fetching, caching, and state strategy. On a conflict, this skill sets the budget and the diagnosis and the framework skill decides the API that meets it. Load a sibling only when the task turns on its layer; if it is not loaded, apply that layer from general knowledge and do not block.
This SKILL.md is self-sufficient: the Ruleset below is the complete, enforceable list. Each
references/<topic>.md holds that group's reasoning and ❌ / ✅ code, and
references/worked-example.md a full review pass; open them for depth when your runtime allows.
How to Use This Skill
Pick the mode that matches the task. Do the steps in order.
| Mode | Steps |
|---|---|
| Generate — build a page or feature with a budget | 1. State the budget for this route — LCP, INP, CLS, and a JS transfer ceiling (budgets). 2. Server-render the LCP element; do not lazy-load it; preload the LCP image and the fonts it needs (lcp, assets). 3. Ship the least JavaScript that works; split by route; keep interactions off the main thread (javascript, inp). 4. Reserve space for every image, embed, and async region (cls). 5. Run the Ruleset as a checklist. Fix each fail before you hand off. |
| Review — check a pull request or a diff | 1. Run the Ruleset against the diff. 2. Write one finding per fail, in the Output Format below, with the metric it costs and a number where you can estimate one. 3. Order the findings: must-fix first, then consider. 4. If nothing fails, say so in one line. Do not invent findings. |
| Diagnose — a page is slow in the field | 1. Read field data first (CrUX / RUM), not a single lab run — know which metric fails at p75 and on which route. 2. For LCP, find the LCP element and walk its critical path (TTFB → render-blocking resources → the resource itself). 3. For INP, capture a trace of the slow interaction and find the long task. 4. For CLS, record the session and identify the shifting element. 5. Fix the top contributor, re-measure in the field, repeat. |
Output Format
Write one finding per line:
<severity> · <topic> · <file>:<line> — <what is wrong>. <the fix as an action>.
<severity>ismust-fix(breaks a budget, or a rule in this skill or a lint rule) orconsider(a real cost that is under the line or unmeasured).<topic>is a Ruleset topic slug (budgets,lcp,inp,cls,javascript,assets,rum).
The targets
Core Web Vitals, measured at the 75th percentile of real users, on mobile, from field data:
| Metric | Good | Meaning |
|---|---|---|
| LCP — Largest Contentful Paint | ≤ 2.5 s | When the main content has rendered. |
| INP — Interaction to Next Paint | ≤ 200 ms | Worst-case tap/keypress-to-paint latency across the visit. |
| CLS — Cumulative Layout Shift | ≤ 0.1 | How much visible content jumps during load. |
Supporting: TTFB ≤ 800 ms, and lab TBT ≤ 200 ms as the pre-release proxy for INP.
Rules for Every Mode
- Name the Ruleset topic when you enforce a rule.
- Tie every claim to a metric and, where you can, a number — "adds ~90 KB gzip to the entry bundle, pushing TBT over 200 ms", not "this is heavy".
- A fix for one metric can cost another: preloading everything delays the LCP resource, over-splitting adds request latency,
font-display: blocktrades CLS for blank text. State the trade. - The budget is p75, mobile, field data. A fast run on a desktop with a wired connection is not a pass.
Ruleset
budgets → references/budgets.md
- Each route has a written budget: LCP, INP, CLS targets plus a ceiling for JS transfer size and request count.
- The budget is enforced in CI — a bundle-size check and Lighthouse CI (or equivalent) on every pull request — and a regression past the ceiling fails the build.
- Third-party scripts have their own byte and main-thread budget; a new one is justified against it, loaded
async/defer, and behind consent where required. - The budget is derived from field data and the competitive set, not a round number picked once.
- A performance change is verified in the field after release, not only in the pull request.
lcp → references/lcp.md
- The LCP element is server-rendered in the initial HTML — not injected by client JavaScript, not inside a client-only component.
- The LCP image or its font is not lazy-loaded, not behind
content-visibility, and carriesfetchpriority="high"; a<link rel="preload">is used only when the resource is discovered late. - Render-blocking CSS is minimal and critical; non-critical CSS and all non-essential JS are deferred.
- TTFB is within budget — cached/streamed HTML, no slow synchronous work in the server render path.
- No above-the-fold web font blocks text for more than 100 ms;
font-display: swaporoptional, with a metric-matched fallback to limit the swap shift.
inp → references/inp.md
- No task on the main thread runs longer than 50 ms during or after load; long work is chunked, yielded (
await scheduler.yield()/setTimeout), or moved to a Web Worker. - An event handler does the minimum synchronously (update state, show feedback) and defers the rest; a non-urgent re-render is marked as such
with the framework's deferred-update API (
react, data-fetching). - Input handlers on high-frequency events (
input,scroll,pointermove) are debounced or throttled and do no layout-thrashing reads-then-writes. - Hydration does not block the first interaction — it is deferred, chunked, or scoped to interactive islands (see
javascript). - A large list is virtualized; expensive derived data is memoized.
cls → references/cls.md
- Every
<img>,<video>,<iframe>, and embed has explicitwidthandheight(or a reservedaspect-ratiobox). - Space is reserved for anything that arrives late — ads, embeds, cookie banners, async content.
- Web fonts use
size-adjust/ascent-overrideor a metric-compatible fallback. - Content is never inserted above existing content in the viewport except in response to a user interaction.
- A region whose height changes (accordion, skeleton → content) animates with
transform, or reserves its final height withmin-height.
javascript → references/javascript.md
- The bundle is inspected with an analyzer before shipping; nothing large or duplicated ships unexplained.
- Code is split at the route boundary, and non-critical below-the-fold widgets are lazy-loaded; the initial route ships only what it needs to render and become interactive.
- Imports are tree-shakeable — named imports from ES modules, no default-importing a whole utility or icon library for one function; a heavy dependency is replaced with a platform API or a smaller one where the need is small.
- Polyfills and transpilation target the supported browser baseline only (
browserslist), with a modern build served to modern browsers. -
modulepreload(or the framework's equivalent) is used for the critical chunk graph.
assets → references/assets.md
- Raster images are served responsively (
srcset+sizes) in a modern format (AVIF or WebP) at the displayed size, through an image CDN or the framework's image component. - Below-the-fold images are
loading="lazy"; all images setdecoding="async"and explicit dimensions. - Fonts are self-hosted, subset to the glyphs used,
woff2, preloaded only for the above-the-fold weights, withfont-displayset. -
preconnectis set for a required cross-origin asset host; unusedpreconnect/dns-prefetchhints are removed. - No layout-critical asset is served from a slow or uncached origin; static assets have a long-lived immutable cache header.
rum → references/rum.md
- Field data is collected in production with the
web-vitalslibrary (or a RUM provider) and read at p75, segmented by route and device class. - INP and LCP are captured with attribution (which element, which script).
- A dashboard tracks the three metrics over time; an alert fires when p75 crosses the target.
- Lab tools (Lighthouse, WebPageTest) are used for diagnosis and pre-release gating, never as the source of truth for how users experience the page.
- A/B tests and experiments report their performance delta, not only the conversion delta.
Limits
This skill is the loading and interaction performance of a web UI. It does not cover:
- Server and infrastructure performance — CDN config, origin autoscaling, database query tuning, edge vs origin rendering trade-offs beyond their TTFB effect.
- Backend API latency and payload design, GraphQL resolver cost, and caching layers on the server.
- Animation smoothness and rendering-pipeline work (
will-change, compositor layers,requestAnimationFramebudgets) beyond their INP and CLS effect. - Build-tool configuration in depth (bundler internals, module federation) beyond the splitting and preload policy here.
- Memory leaks, and native app or React Native performance.
- Choosing metrics targets for a specific business — the CWV "good" thresholds are the floor; a product may set stricter ones.
This skill states the budget and the diagnosis. It is not a substitute for a trace of the real slow interaction and field data at p75.
References
This skill composes with:
react— the rendering and hydration API:<Suspense>andlazy(),useTransition/useDeferredValue, Server Components, streaming SSR,fetchpriorityon<img>. On a conflict this skill sets the budget,reactpicks the API.angular—@deferblocks,NgOptimizedImage, route-level lazy loading,provideClientHydration()and incremental hydration, zoneless change detection.vue—defineAsyncComponentand the router's lazy import,<Suspense>, Nuxt<NuxtImg>/useAsyncData, and the lazy hydration strategies (hydrateOnVisible,hydrateOnIdle).architecture-and-design— data fetching, caching, and the state tiers that decide what renders when and how much ships to the client.accessibility—prefers-reduced-motionand the reflow/zoom requirements that a CLS or font fix must also satisfy.