core-web-vitals-performance-contracts
Installation
SKILL.md
Core web vitals performance contracts
Core Web Vitals are three field metrics — LCP (loading), CLS (visual stability), INP (responsiveness, "good" at 2.5 s / 0.1 / 200 ms) — plus TTFB as an upstream diagnostic. This is a review lens for attributing a failing vital to a specific element, shift, or task and prescribing the narrowest fix; it is not a metrics tutorial (web.dev owns that).
Checklist (lead with the trap)
- Name the real LCP element before you optimize anything. LCP is whatever the largest in-viewport paint is on that load — often an
<img>, but it can be a text block, a<video>poster, or a CSSbackground-image. Read it from a trace /PerformanceObserver/ DevTools, not from "the hero looks big." A preload orfetchpriority="high"aimed at the wrong element wastes bandwidth and can demote the true LCP resource. - The LCP resource must be discoverable early and never lazy. It belongs in the initial HTML
src/srcsetso the preload scanner finds it; late-discovered ones (CSSbackground-image, script-loaded) need<link rel="preload">— and preloaded images still default to low priority, so pair withfetchpriority="high".loading="lazy"on the LCP element is always a bug. - Unblock the critical path and TTFB before micro-tuning. Render-blocking CSS/synchronous
<script>in<head>and a slow TTFB (aim under 800 ms) can make good LCP unreachable regardless of image work — check them and non-streaming SSR before blaming the asset. - Attribute every layout shift to a source — don't just quote the CLS number. Each shift traces to something concrete — an unreserved media/iframe/embed/ad box, a web-font swap with mismatched fallback metrics (FOUT reflow), a consent banner or toast injected above existing content, or an animation of layout properties. Fix the source, not the aggregate.
- Reserve space and animate on the compositor. Give media and late/async content an explicit box (
width/heightoraspect-ratio), tame font swap withfont-display: optionalfor body text or a metric-matched fallback viasize-adjust, and animatetransform/opacity— never layout properties. - INP: break up long tasks and shrink handler work. INP (over 200 ms is poor; it replaced FID) has three phases — input delay, processing, presentation. A long task (>50 ms) overlapping the interaction inflates whichever phase it lands on. Do the minimum in
click/keydown/pointerdownhandlers, defer non-urgent work, and yield withscheduler.yield()/scheduler.postTask()so the browser can paint feedback first — feature-detect each method (globalThis.scheduler?.yield,?.postTask): Safari does not ship the Scheduler API, so keep asetTimeout(0)/requestIdleCallbackchunking fallback. WatchsetIntervaland third-party timers that steal the main thread. - INP: budget hydration and render bursts. A large hydration pass or one big render/state update is a long task that inflates input delay exactly when users first interact (the main thread is busiest at startup). Attribute it with Long Animation Frames (
blockingDuration) rather than guessing which script is at fault.
Quick probes
rg only surfaces candidates — attribution needs a trace: Lighthouse / PageSpeed Insights (lab + CrUX field), the DevTools Performance panel (LCP marker + Layout Shifts track), and the web-vitals library with Long Animation Frames in the field.