Diagnose and improve Core Web Vitals (LCP / CLS / INP). Includes a Mermaid decision tree for root-cause triage across all three vitals + canonical fix-by-symptom map. Field data (CrUX / RUM) at p75 is the measurement target. Used by `performance-engineer` (primary).
Scanned 9/25/2026
npx -y skills add mcorbett51090/RavenClaude --skill core-web-vitals-tuning --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Core Web Vitals Tuning?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/mcorbett51090-core-web-vitals-tuning)More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.
---
name: core-web-vitals-tuning
description: Diagnose and improve Core Web Vitals (LCP / CLS / INP). Includes a Mermaid decision tree for root-cause triage across all three vitals + canonical fix-by-symptom map. Field data (CrUX / RUM) at p75 is the measurement target. Used by `performance-engineer` (primary).
---
# Skill: core-web-vitals-tuning
**Purpose:** Diagnose and improve Core Web Vitals. Used by `performance-engineer` (primary).
## When to use
- CrUX / RUM data shows CWV regression
- Lighthouse audit flags CWV issues
- Pre-launch budget enforcement
- Slow-page complaint from users / stakeholders
- New feature added; verify no perf regression
## The three vitals
| Vital | Threshold (Good) | What it measures |
|---|---|---|
| **LCP** (Largest Contentful Paint) | ≤ 2.5s | Time to render the largest visible element above the fold |
| **CLS** (Cumulative Layout Shift) | ≤ 0.1 | Unexpected layout movement across the page lifecycle |
| **INP** (Interaction to Next Paint) | ≤ 200ms | Worst-case interaction-response time (replaces FID since 2024) |
Field data (CrUX / RUM) at the **75th percentile** is the standard measurement target.
## Decision Tree: CWV root-cause triage
**When this applies:** any CWV regression that needs diagnosis before fix-selection. **Traverse this tree top-to-bottom before picking a fix** — diagnosing the wrong vital wastes the budget.
**Last verified:** 2026-09-23 against the HTTP Archive Web Almanac 2025 Performance chapter, the `web-features` npm package (3.39.0 — `scheduler.yield` ships Chrome/Edge 129+ and Firefox 142+, **not Safari**; `requestIdleCallback` also absent from Safari), and React's CHANGELOG (React 19.3.0, 2026-09-09).
```mermaid
flowchart TD
START[CWV regression reported] --> Q1{"Which vital is failing?<br/>Check CrUX p75"}
Q1 -->|LCP > 2.5s| LCP_Q{What's the LCP element?}
Q1 -->|CLS > 0.1| CLS_Q{When does shift occur?}
Q1 -->|INP > 200ms| INP_Q{Which interaction is slow?}
LCP_Q -->|Image| LCP_IMG["Preload + fetchpriority + WebP/AVIF<br/>+ explicit width/height"]
LCP_Q -->|Text + late web font| LCP_FONT["font-display:swap<br/>+ subset + preload"]
LCP_Q -->|Render-blocking JS / CSS| LCP_JS["Defer JS + critical-CSS inline<br/>+ remove unused CSS"]
LCP_Q -->|Server slow TTFB > 800ms| LCP_TTFB["CDN cache + server-render<br/>+ edge function if dynamic"]
CLS_Q -->|Image / video / iframe| CLS_DIMS["Reserve space:<br/>width+height attrs OR aspect-ratio CSS"]
CLS_Q -->|Late web font swap| CLS_FONT["Match metrics: size-adjust<br/>+ ascent/descent override"]
CLS_Q -->|Ad / embed injected late| CLS_AD["Reserve container min-height<br/>+ lazy-load below the fold"]
CLS_Q -->|Animation triggers reflow| CLS_ANIM["Use transform/opacity only<br/>NOT width/height/top/left"]
INP_Q -->|Heavy JS on click handler| INP_JS["Split work with scheduler.yield<br/>guarded — falls back to setTimeout on Safari<br/>(no scheduler, no requestIdleCallback there)"]
INP_Q -->|Synchronous third-party| INP_3P["Defer/async load<br/>or remove the script"]
INP_Q -->|Large React re-render| INP_REACT["Memoize + virtualize lists<br/>+ React 19 transitions / React Compiler"]
INP_Q -->|Slow paint after JS| INP_PAINT["Use will-change:transform<br/>OR composite-only properties"]
```
**Rationale per leaf:**
- *LCP_IMG* — most common LCP cause. Preload + fetchpriority=high gets the bytes into the browser first; WebP/AVIF cuts bytes 30-50%; explicit dimensions prevent reflow that pushes LCP further out.
- *LCP_FONT* — `font-display:swap` shows fallback immediately; the swap-in is a CLS risk but typically the LCP win is bigger. If CLS becomes an issue, use `size-adjust` to match metrics.
- *LCP_JS* — render-blocking is the silent LCP killer. `defer` on non-critical JS, inline critical CSS, audit with Coverage tab in DevTools.
- *LCP_TTFB* — if TTFB > 800ms, fix the server first. Static / CDN cache for marketing pages. Edge function for dynamic. SSR is fine; CSR is the LCP enemy.
- *CLS_DIMS* — the single highest-impact CLS fix. Every `<img>`, `<video>`, `<iframe>` gets explicit width + height OR aspect-ratio.
- *CLS_FONT* — `size-adjust` + `ascent-override` + `descent-override` match the fallback metrics to the web font so swap-in is invisible.
- *CLS_AD* — late-injected ads or embeds shift everything below. Reserve container `min-height`; lazy-load if below the fold.
- *CLS_ANIM* — animating `width` / `height` / `top` / `left` triggers reflow; animating `transform` / `opacity` is composited and doesn't shift layout.
- *INP_JS* — long tasks block the main thread. `scheduler.yield()` chunks the work in Chrome/Edge 129+ and Firefox 142+, but it is **not supported in Safari** (which also lacks `requestIdleCallback`) — feature-detect and fall back to `setTimeout(fn, 0)` rather than calling `scheduler.yield()` unguarded (it throws a `ReferenceError` in Safari).
- *INP_3P* — synchronous third-party scripts (chat widgets, A/B test SDKs) are common INP killers. Defer/async OR remove.
- *INP_REACT* — large lists virtualize (React Virtuoso, TanStack Virtual); state updates that cascade memoize with `useMemo`/`useCallback`/`React.memo`, or lean on React Compiler's automatic memoization (1.0 shipped 2025-10-07) where enabled; React 19.3 (2026-09-09) also made transitions render independently instead of entangling, which helps interaction responsiveness during concurrent updates.
- *INP_PAINT* — slow paint after JS often means the layer isn't composited. `will-change: transform` hints the browser to promote.
**Tradeoffs summary:**
| Vital | Default fix | Time to fix | Risk |
|---|---|---|---|
| LCP (image) | Preload + fetchpriority + WebP/AVIF | 1-2 hours | Low — well-supported |
| LCP (font) | font-display:swap | 30 min | Medium — may cause CLS if metrics don't match |
| LCP (TTFB) | CDN / static rendering | 4-8 hours | High — may require platform change |
| CLS (dimensions) | width/height attributes everywhere | 2-4 hours | Low |
| CLS (font swap) | size-adjust / ascent-override | 1-2 hours | Low |
| INP (heavy JS) | scheduler.yield + chunking | 4-12 hours | Medium — refactor scope |
| INP (third-party) | Remove or defer | 30 min — 4 hours | Depends on stakeholder pushback |
**Failure modes to avoid:**
- Diagnosing the wrong vital. If CrUX shows LCP > 2.5s but INP is fine, fix LCP first. Don't burn the budget on the wrong vital.
- Treating Lighthouse lab data as field truth. Field (CrUX / RUM) at p75 is the standard target; lab data is a debugging tool, not a goal.
- Optimizing for the median when p75 is what gets graded.
- Fixing CLS by hiding content (visibility:hidden) — that just moves the problem; users still see broken layout.
## LCP — fix-by-symptom
### Symptom: LCP is an image
Most common cause. The fix:
1. Identify the LCP element (Chrome DevTools → Performance → Web Vitals lane).
2. Add `<link rel="preload" as="image" href="..." fetchpriority="high">` in `<head>`.
3. Add `fetchpriority="high"` to the `<img>` itself.
4. Remove `loading="lazy"` from the LCP image.
5. Serve it in modern format (AVIF / WebP).
6. Right-size it (responsive `srcset` + `sizes`); don't ship a 4000px image to a 400px slot.
7. Compress aggressively (90-quality WebP is usually indistinguishable from 100).
8. Serve via image CDN.
### Symptom: LCP is text, blocked by web font
1. Self-host fonts (or use a fast CDN).
2. `font-display: swap` (or `optional`).
3. Preload critical fonts: `<link rel="preload" href="..." as="font" type="font/woff2" crossorigin>`.
4. Subset fonts (Latin only when content is Latin; drop unused weights).
5. Reduce number of weights / styles loaded.
### Symptom: LCP blocked by render-blocking CSS
1. Inline critical CSS (above-the-fold styles) in `<head>`.
2. Defer non-critical CSS via `<link rel="preload" as="style" onload="this.rel='stylesheet'">` pattern.
3. Avoid `@import` chains.
### Symptom: LCP blocked by render-blocking JS
1. `defer` or `async` every non-essential script.
2. Move analytics / chat / A/B-test scripts to load post-interaction or post-LCP.
3. Code-split the framework if possible (Astro / Next handle this; vanilla SPAs need manual care).
### Symptom: Server response slow (TTFB > 600ms)
1. Move to a CDN-edge-rendered platform (Vercel / Netlify / Cloudflare Pages).
2. Static-generate pages (SSG) where possible; SSR + edge caching otherwise.
3. Investigate backend / DB latency (escalate to `ravenclaude-core/backend-coder`).
## CLS — fix-by-symptom
### Symptom: Image shifts in
1. Always specify `width` and `height` (or `aspect-ratio`) on `<img>`.
2. Use `aspect-ratio` CSS for responsive containers.
### Symptom: Web font swaps in, text reflows
1. `font-display: optional` (no swap; user without font cached sees system font; LCP not affected by font).
2. Or `font-display: swap` + `size-adjust` / `ascent-override` / `descent-override` on `@font-face` to match system-font metrics.
### Symptom: Ad / embed loads in late, pushes content down
1. Reserve space with `min-height` or `aspect-ratio` for the ad / embed container.
2. Defer ad / embed load until below-the-fold visibility (intersection observer).
### Symptom: Element animated using `width` / `height` / `top` / `left`
1. Animate `transform` and `opacity` instead.
2. Add `will-change: transform` where genuinely needed (don't sprinkle widely).
### Symptom: Cookie banner / notification jumps content
1. Position as overlay (fixed / absolute) so it doesn't reflow content.
2. Or reserve space for it from initial render.
## INP — fix-by-symptom
### Symptom: Click handler runs long task on main thread
1. Break long tasks into smaller chunks (yield to main thread with `await new Promise(r => setTimeout(r, 0))` or `scheduler.yield()`).
2. Move CPU work to a Web Worker.
3. Debounce / throttle high-frequency handlers.
### Symptom: React re-renders too much on interaction
1. Memoize expensive components.
2. Move state down (lift fewer state changes high in the tree).
3. Use `useDeferredValue` / `useTransition` for non-urgent updates.
### Symptom: Hydration on a partially-static site
1. Astro islands / Qwik / partial hydration.
2. Don't hydrate components that don't need interactivity.
### Symptom: Third-party script blocks main thread
1. Catalogue every third-party script + its main-thread cost.
2. Defer non-critical (analytics, chat, A/B test) until after Idle.
3. Replace with first-party where possible.
## Measurement tooling
- **CrUX**: PageSpeed Insights for field data
- **RUM**: web-vitals.js library, Vercel Speed Insights, Cloudflare Web Analytics, custom integration
- **Lab**: Lighthouse (DevTools / CLI), WebPageTest (slow 3G + Moto G4 simulates real mobile)
**Field beats lab.** Always show field data in audit reports when available.
## Performance budget
Set per-page budgets:
| Metric | Marketing site | Product app |
|---|---|---|
| LCP (P75) | ≤ 2.5s | ≤ 2.5s |
| CLS (P75) | ≤ 0.05 | ≤ 0.1 |
| INP (P75) | ≤ 200ms | ≤ 200ms |
| Page weight (transfer) | ≤ 1 MB | ≤ 2 MB |
| JS bundle (compressed) | ≤ 100 KB | ≤ 250 KB |
| Image budget | ≤ 500 KB | ≤ 1 MB |
| Third-party scripts | ≤ 3 | ≤ 5 |
Enforce in CI where possible (Lighthouse CI, bundle-size action).
## See also
- Template: [`../../templates/performance-budget.md`](../../templates/performance-budget.md)
- Agent: [`../../agents/performance-engineer.md`](../../agents/performance-engineer.md)
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!