Core Web Vitals skill for Codex
Fix the LCP, INP or CLS phase that actually dominates. Codex starts from field data, not a desktop Lighthouse run, then breaks the failing metric into its phases: TTFB, load delay, load duration and render delay for LCP; input delay, processing and presentation for INP; shift culprits for CLS. Each phase maps to a specific fix, applied one at a time and re-measured, with a realistic expectation of when CrUX will reflect it.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- core-web-vitals Installs to ~/.agents/skills/core-web-vitals/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install core-web-vitals The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project core-web-vitals Installs to .agents/skills/core-web-vitals/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/core-web-vitals.zipunzip -o core-web-vitals.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) core-web-vitals PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $core-web-vitals, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Diagnoses and fixes Core Web Vitals failures - LCP, INP and CLS - by splitting each metric into its sub-parts (TTFB, load delay, render delay; input delay, processing, presentation; shift culprits), finding the element or interaction responsible, and applying the matching fix. Use when Search Console or CrUX reports poor or needs-improvement URLs, when PageSpeed Insights flags LCP, INP or CLS, or when a page feels slow to show content, laggy on click, or jumpy while loading.
Use it when
- Search Console or CrUX lists URLs as poor or needs improvement
- PageSpeed Insights shows a failing LCP, INP or CLS
- Users report slow first content, laggy clicks or jumping layout
Use something else when
- You need a CI gate on Lighthouse scores rather than a diagnosis: use lighthouse-audits
- You have no field data yet and need to start collecting it: use rum-monitoring
How to check the result
The dominant sub-part shrinks across five median lab runs with no other metric regressing, then RUM or CrUX p75 for the URL group moves into the good band.
The mistake it prevents
Lazy-loading every image, including the LCP hero, which delays the very element the metric measures.
Done well means: every fix targets the sub-part that actually dominates the metric, the result is confirmed against field data at the 75th percentile, and nothing was “fixed” on a fast desktop lab run that real phones never see.
Thresholds (p75 of page loads, mobile and desktop separately)
| Metric | Good | Needs improvement | Poor | What it measures |
|---|---|---|---|---|
| LCP | ≤ 2.5 s | ≤ 4.0 s | > 4.0 s | Render time of the largest image or text block in the viewport |
| INP | ≤ 200 ms | ≤ 500 ms | > 500 ms | Worst-ish interaction latency: input to next paint |
| CLS | ≤ 0.1 | ≤ 0.25 | > 0.25 | Largest burst (session window) of unexpected layout shift |
A URL passes only when all three are good at p75. FID is gone; never optimise for it.
Workflow
- [ ] 1. Get field data: which metric fails, on which device class, on which URL group
- [ ] 2. Reproduce in the lab with throttling that matches the failing segment
- [ ] 3. Break the failing metric into sub-parts and find the dominant one
- [ ] 4. Identify the culprit element, script or interaction
- [ ] 5. Apply the fix for that sub-part (tables below), one change at a time
- [ ] 6. Re-measure in the lab, then confirm in field data after 28 days (CrUX) or sooner via RUM
1. Field data first
- Search Console “Core Web Vitals” groups failing URLs by template; fix templates, not URLs.
- PageSpeed Insights top section shows CrUX field data for the URL and origin.
- CrUX API / BigQuery need network and an API key; ask before calling them.
- If the project ships its own RUM (look for
web-vitalsimports), use it: it has the attribution data CrUX lacks. See$rum-monitoringto set that up.
Field and lab disagree for good reasons: lab has no real interactions (INP is unmeasurable there; TBT is a proxy), no user scrolling (CLS after load is missed), and a cold cache. Trust field data for “is it fixed”, lab data for “why”.
2. Reproduce
Chrome DevTools Performance panel: set CPU throttling to 4x or 6x and network to “Slow 4G” for mobile segments, record a reload, and read the LCP marker, layout-shift track and interactions track. The Performance panel’s insights sidebar names the LCP element, its phases and the CLS culprits directly.
npx lighthouse https://example.com/product/1 --only-categories=performance \
--output=json --output-path=./lh.json --chrome-flags="--headless=new"
Needs network for npx and the target URL. For repeatable gating use $lighthouse-audits.
LCP: find the dominant phase
LCP = TTFB + resource load delay + resource load duration + element render delay.
| Phase dominates | Typical cause | Fix |
|---|---|---|
| TTFB (> ~40% of LCP) | Slow server, uncached HTML, redirects | Cache HTML at the edge, stream, kill redirects: $ttfb-optimization |
| Load delay | LCP image discovered late (CSS background, JS-rendered, loading="lazy") | Put it in HTML as <img>, add fetchpriority="high", never lazy-load it, or <link rel="preload" as="image"> |
| Load duration | Image too heavy, wrong format, no CDN | Resize to rendered size, AVIF/WebP, CDN: $image-optimization |
| Render delay | Render-blocking CSS/JS, web font hiding text, client-side rendering, A/B test hiding the page | Inline critical CSS, defer JS, font-display: swap or optional, server-render the hero |
<!-- LCP hero: discoverable in HTML, high priority, sized, not lazy -->
<img src="/img/hero-1200.avif" width="1200" height="600" alt="..."
fetchpriority="high" decoding="async">
Next.js: give only the LCP image preload (Next 16+; priority before 16) or
fetchPriority="high"; never on below-the-fold images.
Anti-flicker snippets from A/B tools hide the whole body until the experiment script
loads; that shows up as huge render delay. Flag them to the user rather than removing
them silently: $third-party-scripts.
INP: find the slow interaction, then the slow phase
INP = input delay + processing duration + presentation delay, for one of the slowest interactions on the page.
- Use RUM attribution (
interactionTarget,interactionType, phase durations,longAnimationFrameEntries) to learn which element and which phase. Without RUM, interact manually in DevTools with CPU throttling and inspect the interaction track. - Read the Long Animation Frame’s script attribution: which function, which file.
| Phase dominates | Cause | Fix |
|---|---|---|
| Input delay | Main thread busy when user clicks (hydration, third-party, timers) | Break up long tasks, defer non-critical JS, delay third-party until idle |
| Processing | Event handler does too much synchronously | Do the visual update first, yield, then the rest; move heavy compute to $web-workers |
| Presentation | Huge DOM, layout thrash, expensive style recalcs | Reduce DOM size, content-visibility: auto, batch reads before writes, virtualise lists |
Yield pattern (paint feedback before heavy work):
async function yieldToMain() {
if (globalThis.scheduler?.yield) return scheduler.yield();
return new Promise((r) => setTimeout(r, 0));
}
button.addEventListener('click', async () => {
button.classList.add('is-loading'); // cheap visual response first
await yieldToMain(); // let the browser paint
applyFilters(bigList); // heavy work after the frame
});
React apps: wrap non-urgent state updates in startTransition, and see
$react-render-profiling when processing time is component re-rendering.
CLS: find the shifting element and its cause
DevTools Performance panel “Layout shifts” track or RUM largestShiftTarget names it.
| Cause | Fix |
|---|---|
| Images, video, iframes without dimensions | width/height attributes or CSS aspect-ratio |
| Ads, embeds, consent banners injected above content | Reserve space with min-height; render overlays as fixed/sticky, not in flow |
| Web font swap changes text metrics | Fallback font with size-adjust, ascent-override; font-display: optional for body text |
| Content inserted after fetch (banners, “related items”) | Reserve a skeleton of the final size, or insert below the viewport |
Animating top/left/height | Animate transform and opacity only |
| bfcache disabled, so back nav re-lays out | Remove unload handlers and Cache-Control: no-store on HTML where safe |
Shifts within 500 ms of a user input do not count; shifts during scroll from lazily loaded content without reserved space do.
Hard rules
- Never
loading="lazy"on the LCP element. Lazy-loading everything is a common LCP regression. fetchpriority="high"on one or two resources, not ten; priority is relative.- Preloading a resource the parser would find anyway wastes bandwidth; preload only late-discovered critical resources.
- Measure mobile separately. A desktop pass hides most failures.
- One change per measurement cycle, or you cannot attribute the improvement.
- Lab LCP with an empty cache over-states repeat-visit LCP; field p75 mixes both.
Verification
- Lab: the dominant sub-part shrank by the expected amount across 5 runs (median), and no other metric regressed.
- Field: RUM p75 for the affected URL group moves within days; CrUX reflects it over a 28-day rolling window, so do not declare failure after one week.
- If the metric did not move, the sub-part analysis was wrong: re-check which phase dominates on real devices (RUM attribution) before trying another fix.
Report template
## Core Web Vitals: <URL group / template>
Field (p75, mobile): LCP 3.8 s | INP 310 ms | CLS 0.02 Source: CrUX / RUM, date range
Failing: LCP (load delay 1.6 s of 3.8 s), INP (processing 190 ms on "Add to cart")
| Fix | Metric / phase | Lab before -> after (median of 5) |
|---|---|---|
| Hero moved from CSS background to <img fetchpriority=high> | LCP load delay | 1.6 s -> 0.2 s |
Expected field impact: ... Re-check on: <date + 28 days>
Not addressed: <third-party anti-flicker snippet, needs owner sign-off> What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Core Web Vitals"
short_description: "Fix the LCP, INP or CLS phase that actually dominates"
default_prompt: "$core-web-vitals Search Console says our product pages fail LCP on mobile, find out why and fix it" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.