Web Vitals Optimizer
Diagnose and fix Core Web Vitals — LCP, CLS, and INP — by treating real-user field data at p75 as the source of truth, using Lighthouse/WebPageTest only to find the at-fault element, script, or shift, then applying the one targeted fix per metric and re-measuring. Use when a page feels slow, scores poorly on PageSpeed/Lighthouse, or fails CWV in CrUX/RUM field data.
npx agentscamp add skills/web-vitals-optimizerInstall to ~/.claude/skills/web-vitals-optimizer/SKILL.md
A green Lighthouse score with failing field data is the most common Web Vitals trap. This skill anchors on p75 real-user CrUX/RUM data, uses lab tools only to pinpoint the at-fault element/script/shift, and fixes each of LCP, CLS, and INP by its actual cause — then re-measures against the field target.
A page can score 98 in Lighthouse and still fail Core Web Vitals for real users — because Lighthouse measures one throttled load on your machine, while Google ranks you on p75 of field data from real devices and networks. This skill refuses to optimize the lab number. It pulls the field metrics first, uses lab tools only to find the specific element, script, or shift at fault, applies the one fix that addresses that cause, and re-measures against the field target — not the audit.
When to use this skill
- A page is flagged "Needs improvement" or "Poor" for LCP, CLS, or INP in Search Console / CrUX / your RUM, even if Lighthouse looks fine.
- The hero or main content visibly pops in late, or the page jumps as images, ads, fonts, or banners load.
- Tapping a button, opening a menu, or typing feels laggy after the page looks ready.
- You're about to "fix performance" by chasing a higher Lighthouse score and want to target what real users actually feel.
Instructions
- Get the field data first — it is the only source of truth. Pull p75 LCP, CLS, and INP from CrUX (PageSpeed Insights field section, the CrUX API, or BigQuery) for the specific URL or origin, segmented by phone vs. desktop. If you have RUM (
web-vitalslibrary, your analytics), prefer it — it's per-page and current. Thresholds: LCP ≤ 2.5s, CLS ≤ 0.1, INP ≤ 200ms, all at p75. Write down the failing metric(s) and the gap to target before opening a single file. - Use lab tools only to find the culprit, never as the goal. Run Lighthouse / WebPageTest / a local trace to locate what's at fault — the LCP element, the layout-shift sources, the long tasks blocking interaction. The lab gives you the "what and where"; the field data decides whether you've actually won. A green lab score does not close a failing field metric.
- LCP — find the LCP element, then speed its delivery. Read the Lighthouse "Largest Contentful Paint element" (usually the hero image or a large heading/text block). If it's an image: ensure it is not
loading="lazy", addfetchpriority="high",<link rel="preload" as="image">it (withimagesrcset), serve a right-sized AVIF/WebP at the displayed dimensions, and host it on a fast/CDN origin. If it's blocked by render-blocking CSS/JS, inline critical CSS anddefer/async the rest. If TTFB itself is slow (>800ms), fix the server/cache before touching the front end — you can't paint what hasn't arrived. - CLS — reserve space and stop late insertions. For every image/video/iframe/ad/embed, set explicit
width/heightoraspect-ratioso the browser reserves the box before content loads. Never inject content above existing content after load (cookie/consent banners, late-arriving ads, "you have a new message" bars) — reserve their slot or render them in a fixed overlay. For font swap,<link rel="preload">the font and usefont-display: optionalor asize-adjust/ascent-override@font-faceto match fallback metrics so the swap doesn't reflow text. - INP — shorten the work between tap and paint. Find the slow interaction in a performance trace and read the long tasks (>50ms) on the main thread. Break long JS into chunks and
yieldto the main thread (await scheduler.yield()orsetTimeout(0)) so input can be handled; defer or remove unnecessary hydration and heavy third-party scripts (analytics, chat, A/B tools) that monopolize the thread; keep event handlers cheap — do the visual update first, then debounce/queue the expensive work. Don't run layout-thrashing reads/writes inside the handler. - Change one thing, then re-measure against the field metric. After each fix, re-run the lab trace to confirm the mechanism (LCP element now preloaded, shift gone, long task split). But only the p75 field metric trending back under threshold confirms a real win — and field data lags 28 days in CrUX, so verify with RUM for fast feedback. If the field metric doesn't move, you fixed the wrong cause; go back to the trace.
WARNING
Optimizing the Lighthouse lab score while p75 field data still fails is optimizing the wrong number. Lighthouse is one throttled synthetic load; CrUX is the 75th percentile of real devices and networks, and that is what ranks. Ship for the field metric — a 100 lab score with "Poor" field LCP is still a failing page.
NOTE
A blanket loading="lazy" on every image directly regresses LCP when it lands on the hero/above-the-fold image — the browser delays the very request that defines your LCP. Lazy-load only below-the-fold media; the LCP image must be eager and, ideally, preloaded with fetchpriority="high".
Output
Per failing metric: the specific culprit (the named LCP element, the elements/sources causing each shift, or the long-task script/handler), the single targeted fix as an Edit diff (preload tag, width/height, defer, yield, etc.), and the p75 field target to confirm against (LCP ≤ 2.5s / CLS ≤ 0.1 / INP ≤ 200ms) with a note on how to verify it (RUM now, CrUX after the 28-day window). End with the lab mechanism check plus the field metric as the real pass/fail gate.
Related
- Bundle AnalyzerAnalyze a JS/TS production bundle and surface the biggest size wins — heavy dependencies, duplicate packages, missing code-splitting, oversized polyfills, and dev/server code leaking into the client. Use when a bundle is too large and you need a ranked, actionable reduction plan.
- React Render ProfilerFind and fix wasteful React re-renders by classifying the cause — unstable prop/callback/object identities, context value churn, state lifted too high, expensive work in render, or unvirtualized lists — confirming it with a measurement, then applying the one targeted fix and re-measuring. Use when a React UI is janky, slow to type in, or re-renders far more than the data actually changed.
- Cold Start OptimizerCut cold-start latency for serverless functions and slow-booting apps by measuring the init breakdown, then attacking the dominant phase — artifact size, eager imports, eager connections, or under-provisioned memory — instead of reflexively buying provisioned concurrency. Use when serverless p99 spikes on the first request, when a function times out during init, or when scale-to-zero is hurting user-facing latency.