| name | web-vitals-budgets |
| description | Browser-measured Core Web Vitals (LCP/CLS/INP) budgets and measurement workflow for hero video, animated headers, and immersive media surfaces. Use when shipping or auditing any above-the-fold media change to skyyrose.co, and during launch audits — the browser half that wp-performance (backend-only) explicitly excludes. Do NOT use for backend TTFB root-causing, DB/query/cron/object-cache work (that is wp-performance), or for changes that cannot alter above-fold rendering. |
Web Vitals Budgets — Media-Heavy Surfaces
wp-performance explicitly opts out of browser measurement. This skill is the browser half: real
LCP/CLS numbers via Chrome DevTools MCP or Playwright, with budgets for the media patterns this
site actually ships.
When to use
- An above-the-fold media change (hero video, animated header, image strip, lockup image) is about
to ship or just shipped to skyyrose.co — measure before AND after.
- A launch audit needs LCP/CLS/INP numbers with element attribution.
- A CSS/JS change altered above-fold layout — CLS must be re-measured, not just visually QC'd.
When NOT to use:
- Backend slowness (TTFB, REST, admin, cron) →
wp-performance. This skill only attributes TTFB
inside the LCP breakdown; it does not fix it.
- Changes with no above-fold rendering impact (PHP-only logic, admin screens, email templates).
- Judging design quality — this skill produces milliseconds and shift scores, not taste.
Inputs
Absent input = STOP. Numbers, not vibes — no browser tooling means no verdict.
- A reachable target URL (production
https://skyyrose.co/... or a preview). Probe it first
(see Verification check 1); a non-200 or sub-50KB response means measure nothing yet.
- Chrome DevTools MCP or Playwright MCP available. If neither is available this session, the
LCP/CLS/INP checks are SKIPs, not PASSes — say so explicitly and name who closes them
(the caller re-runs with browser tooling). Do not substitute a curl for a CLS number.
- The budgets table below — a measurement without a budget to compare against is trivia.
- Logged-OUT context — the WP admin-bar shifts everything 32px and corrupts CLS.
Budgets (skyyrose.co, mobile 4G reference)
| Metric | Budget | Hard fail |
|---|
| LCP | ≤ 2.5s (excl. known TTFB debt ~2.3-3.4s — track separately, don't hide it) | > 4.0s |
| CLS | ≤ 0.05 | > 0.1 |
| INP | ≤ 200ms | > 500ms |
| Above-fold media weight | ≤ 1.5MB total | > 2.5MB |
| Header animated asset | ≤ 400KB | > 1MB |
Procedure
- Baseline TTFB + reachability with cache-busted curl (Verification check 1). Record whether
the edge cache HIT or MISS — the
server-timing/x-ac headers say which; a MISS number and a
HIT number are different experiments and must not be compared.
- Chrome DevTools MCP:
performance_start_trace → navigate cold (cache disabled) →
performance_stop_trace → performance_analyze_insight for the LCP breakdown (TTFB / load
delay / load time / render delay). Mobile viewport 390×844 AND desktop 1440×900.
- Playwright fallback:
browser_evaluate → new PerformanceObserver for
largest-contentful-paint + layout-shift entries; report element attribution for LCP (WHICH
element — if the header video steals LCP candidacy from the hero, that's a regression even at
the same ms).
- Three runs, report median. One run = noise.
- Before/after on the SAME network conditions — deploy comparisons across different cache states
are invalid.
- Report in the format:
LCP 2.31s (el: img.hero-lockup, ttfb 1.9s) | CLS 0.02 | INP 140ms | mobile, 3-run median, cold — element attribution + conditions or the number doesn't count.
Verification
A trace that errored or timed out is an artifact, not a green result — re-run it (bug-230). Each
check states its pass condition; each can fail.
curl -s -o /dev/null -w 'code=%{http_code} ttfb=%{time_starttransfer}s total=%{time_total}s size=%{size_download}B\n' \
"https://skyyrose.co/?cb=$(date +%s)"
PASS: code=200, size ≥ 50000B. Record ttfb — it is the floor under LCP (LCP can never beat
TTFB + render). Observed 2026-07-28: code=200 ttfb=1.888431s total=2.092811s size=136605B on an
edge-cache MISS. [live]
new Promise((resolve) => {
let lcp = null, cls = 0;
new PerformanceObserver((l) => { const e = l.getEntries().at(-1);
lcp = { t: e.startTime, el: e.element?.tagName + '.' + (e.element?.className || '') };
}).observe({ type: 'largest-contentful-paint', buffered: true });
new PerformanceObserver((l) => { for (const e of l.getEntries())
if (!e.hadRecentInput) cls += e.value;
}).observe({ type: 'layout-shift', buffered: true });
setTimeout(() => resolve({ lcp, cls: +cls.toFixed(4) }), 6000);
});
PASS: median of 3 runs → LCP ≤ 2.5s AND the attributed element is the intended hero (not the
nav video), AND CLS ≤ 0.05. Hard fail at LCP > 4.0s or CLS > 0.1 per the budgets table. [live]
curl -s -o /dev/null -w '%{size_download}\n' "https://skyyrose.co/<header-asset-path>?cb=$(date +%s)"
PASS: header animated asset ≤ 409600 bytes (400KB budget); above-fold media sum ≤ 1572864
bytes. [live]
Prove the CLS check can fail (rule 3): once per harness setup, strip width/height off the hero
<img> in a local copy and re-measure — CLS must go red — then restore. A CLS gate never observed
failing is a guess with a citation.
Worked example
Real probe of production, 2026-07-28, from this repo:
curl -s -o /dev/null -w 'code=%{http_code} ttfb=%{time_starttransfer}s total=%{time_total}s size=%{size_download}B\n' \
"https://skyyrose.co/?cb=$(date +%s)"
curl -sI "https://skyyrose.co/?cb=$(date +%s)" | grep -iE "^(HTTP|x-ac|server-timing)"
Observed output [live]:
code=200 ttfb=1.888431s total=2.092811s size=136605B
HTTP/2 200
x-ac: 1.sjc _atomic_bur MISS
server-timing: a8c-cdn, dc;desc=sjc, cache;desc=MISS;dur=2493.0
Reading it: the cache-busted request MISSed the WP.com edge (cache;desc=MISS;dur=2493.0), so
1.888s TTFB is the origin cost — consistent with the known ~2.3-3.4s TTFB debt band. Any LCP
measured on this cold path carries that debt; report it separately (ttfb 1.9s inside the LCP
line) instead of hiding it, and also measure the warm-cache path real visitors mostly hit. The
browser half (LCP element attribution, CLS) still requires the Playwright/DevTools run — this curl
alone is [live] evidence for TTFB and reachability ONLY, never for LCP/CLS (bug-287: evidence
scope must cover claim scope).
Failure modes
- CLS traps specific to this theme:
.min rebuild changing rendered sizes: any CSS edit that alters above-fold layout re-measures
CLS, not just visual QA (production serves .min — measure after npm run build).
- WP admin-bar (logged-in) shifts everything 32px — measure logged-OUT.
- Batcache/edge cache serves stale HTML referencing new CSS versions (or vice versa) during
deploy windows — measure after cache settles or with cache-busted URL, and note which.
- WebFetch on live HTML is banned — it strips
<script>, so anything script-injected (LCP
candidates included) silently vanishes. Use curl with ?cb=$(date +%s).
- Blank Playwright screenshots: call
img.decode() on hero images before screenshotting, or
the capture races the decode and you QC an empty frame.
- Header video stealing LCP candidacy from the hero at "same ms" — always check the attributed
element, not just the number. Small +
fetchpriority="low" on nav media; fetchpriority="high"
on the ONE intended LCP element.
- Poster is the LCP candidate for hero video — optimize it like a hero image (≤200KB),
preload="metadata", autoplay muted loop playsinline, explicit width/height.
- Auto-scrolling strips:
content-visibility: auto off-screen, transform-only animation
(compositor thread), loading="lazy" beyond first viewport, ≤100KB/image.
- A dead trace read as green (bug-230): DevTools trace that errors/timeouts has no findings
because it has no data — re-run, never report it as passing.
- Scope jump (bug-287): a repo-side asset diff is not a live vitals claim — "regressed LCP"
requires the
[live] probe.