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.

  • Performance
  • $core-web-vitals
  • 165 lines
  • 2 files
  • ~2,000 tokens
  • MIT
  • Updated
  • Editor's pick

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- core-web-vitals

Installs to ~/.agents/skills/core-web-vitals/ for every project on this machine.

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.

~/.agents/skills/core-web-vitals/SKILL.md

--- name: core-web-vitals description: 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… license: MIT ---

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)

MetricGoodNeeds improvementPoorWhat it measures
LCP≤ 2.5 s≤ 4.0 s> 4.0 sRender time of the largest image or text block in the viewport
INP≤ 200 ms≤ 500 ms> 500 msWorst-ish interaction latency: input to next paint
CLS≤ 0.1≤ 0.25> 0.25Largest 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-vitals imports), use it: it has the attribution data CrUX lacks. See $rum-monitoring to 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 dominatesTypical causeFix
TTFB (> ~40% of LCP)Slow server, uncached HTML, redirectsCache HTML at the edge, stream, kill redirects: $ttfb-optimization
Load delayLCP 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 durationImage too heavy, wrong format, no CDNResize to rendered size, AVIF/WebP, CDN: $image-optimization
Render delayRender-blocking CSS/JS, web font hiding text, client-side rendering, A/B test hiding the pageInline 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.

  1. 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.
  2. Read the Long Animation Frame’s script attribution: which function, which file.
Phase dominatesCauseFix
Input delayMain thread busy when user clicks (hydration, third-party, timers)Break up long tasks, defer non-critical JS, delay third-party until idle
ProcessingEvent handler does too much synchronouslyDo the visual update first, yield, then the rest; move heavy compute to $web-workers
PresentationHuge DOM, layout thrash, expensive style recalcsReduce 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.

CauseFix
Images, video, iframes without dimensionswidth/height attributes or CSS aspect-ratio
Ads, embeds, consent banners injected above contentReserve space with min-height; render overlays as fixed/sticky, not in flow
Web font swap changes text metricsFallback 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/heightAnimate transform and opacity only
bfcache disabled, so back nav re-lays outRemove 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.yamlgenerated from this page

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.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem