Web Performance Operations

SkillWeb & browsing

Frontend page speed and Core Web Vitals (LCP, INP, CLS), method first: field data (CrUX, RUM) vs lab (Lighthouse, WebPageTest), find the failing metric's subpart, then fix images, fonts, JavaScript and third-party tags, critical CSS and caching/CDN headers, and lock wins with Lighthouse CI budgets. Covers Craft CMS levers (Blitz, eager loading, transforms, craft-vite). Use when pages load or respond slowly, PageSpeed/Lighthouse scores drop, or Search Console flags Core Web Vitals.

Instructions available. Your AI can read the instructions. Execution depends on the setup they require.

Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.

Then ask your AI: use the Web Performance Operations skill

What this skill tells your AI

The instructions your AI receives, as published by 0xdarkmatter/claude-mods in skills/web-perf-ops/SKILL.md and read by ahel’s review.

Verified against web.dev + Chrome docs (2026-10-05). Every threshold below lives once in assets/web-perf-facts.json. scripts/check-web-perf-facts.py --offline fails if this table and the catalog disagree, and --live (weekly) fails if Google moves a number.

Diagnose and fix frontend page speed the way Core Web Vitals are judged: field data decides whether there is a problem, lab data explains it, and each fix targets the failing metric's dominant subpart, not "the score". Server-side profiling, load testing and backend latency belong to perf-ops; this skill takes over once the question is about what a browser experiences.

The method

1 FIELD   Is it real? CrUX p75 (PSI field block, CrUX API, Search Console) or your RUM.
          Phone and desktop separately. No field data -> say the verdict is lab-only.
2 METRIC  Which Core Web Vital fails, and which SUBPART:
            LCP = TTFB | load delay | load duration | render delay
            INP = input delay | processing | presentation      CLS = which element, why
3 LAB     Reproduce for a trace: Lighthouse, DevTools Performance panel, WebPageTest
          (or the cloudflare:web-perf skill, if installed). Lab explains; field judges.
4 FIX     Fix the dominant subpart. One change per measurement.
5 PROVE   Lab before/after (median of 5 runs) -> ship -> field confirms over 28 days
          -> lock it with a CI budget so it cannot regress.

Skipping step 1 is the classic failure: optimising a Lighthouse score that users never felt, while the real problem (INP after the consent banner, CLS three screens down) is invisible in the lab.

Thresholds

Assessed at the 75th percentile of page loads. Mobile and desktop are assessed separately against the same numbers. INP replaced FID as a Core Web Vital on 2024-03-12.

MetricGoodPoorMeasuresField / lab
LCP<= 2.5 s> 4 sLoading: largest image/text block paintedBoth
INP<= 200 ms> 500 msResponsiveness: slowest interaction to next paintField; lab only for an interaction you perform (load proxy: TBT)
CLS<= 0.1> 0.25Visual stability: largest burst of unexpected shiftsBoth (lab sees load only)
FCP<= 1.8 s> 3 sFirst paint (diagnostic, not a CWV)Both
TTFB<= 800 ms> 1.8 sServer + network (diagnostic; web.dev calls it a rough guide)Both
TBT<= 200 ms> 600 msMain-thread blocking during load (Lighthouse scoring points)Lab only

Between the two columns is "needs improvement". The Lighthouse performance score is a weighted lab composite (TBT 30, LCP 25, CLS 25, FCP 10, Speed Index 10). It is not a Core Web Vital and not a ranking signal.

Step 1: which number to trust

SituationTrustNext move
Field poor, lab fineFieldLab isn't reproducing users: throttle, cold cache, consent accepted, scroll, mid-range phone; get attribution from RUM
Field good, lab poorFieldNo user-facing problem today; treat lab findings as regression risks
No field data for the URLOrigin-level CrUX, then labState that the verdict is lab-only
Fix just shippedLab now, field laterCrUX is a rolling 28-day window; watch the History API's weekly points or RUM
INP failsRUMLab page loads have no interactions; find the slow interaction in RUM, then reproduce it in DevTools
CrUX and RUM disagreeBothCrUX is Chrome-only; segment RUM to Chrome to compare

Details, tool defaults (Lighthouse 13 emulates a Moto G Power on slow 4G with 4x CPU) and a RUM snippet for web-vitals v6: field-vs-lab.md.

Step 2: name the metric and its subpart

MetricRead the breakdown fromDominant subpart -> first fix
LCPCrUX image-LCP subparts; RUM onLCP attribution; Lighthouse lcp-breakdown-insightTTFB: page/edge caching. Load delay: hero lazy-loaded, CSS background or JS-injected, missing fetchpriority. Load duration: oversized image or format. Render delay: render-blocking CSS/JS, fonts, fade-in animation
INPRUM onINP attribution (interactionTarget, phases, longestScript, loadState)Input delay: load-time JS and third-party tags. Processing: handler work, so paint first and yield. Presentation: DOM size, layout cost
CLSRUM onCLS largestShiftTarget; DevTools layout-shift track; cls-culprits-insightUnsized images/embeds, injected banners, font swaps, late CSS, scroll-triggered content

The healthy LCP profile is about 40% TTFB, under 10% load delay, about 40% load duration and under 10% render delay. The two delays are pure waste; check them first.

Step 3: triage a report

scripts/triage-vitals.py turns whichever report you have into rated rows that each name the reference holding the fix. It accepts a Lighthouse JSON, a PageSpeed Insights API response or a CrUX API / History API response, and normalises their different encodings (PSI sends CLS x100 as an integer; CrUX sends it as a string).

# PSI: field (URL + origin) + one lab run. Use an API key: keyless calls hit a quota of 0.
curl -s "https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=https%3A%2F%2Fexample.com%2F&strategy=mobile&key=$PSI_KEY" > psi.json
python scripts/triage-vitals.py psi.json
# field:psi-url   LCP    4.20 s   poor               references/lcp.md
# field:psi-url   CLS    0.12     needs-improvement  references/cls.md
# lab:audit       image-delivery-insight  LCP -900 ms  opportunity  references/images.md

python scripts/triage-vitals.py crux.json --json | jq '.data[] | select(.rating != "good")'

Field rows come first. With CrUX it also prints the four field LCP subparts and marks the dominant one. Lab audits are listed only when they claim metricSavings. Exit 10 = at least one metric needs work, 0 = all rated metrics good, 4 = unrecognised report.

Step 4: fix map

SymptomFixReference
High TTFB on a CMS siteFull-page static cache + HTML at the CDN edge with purge-on-publish; kill redirect chainscaching-cdn.md, craft.md
LCP image discovered late<img> in server HTML, eager, fetchpriority="high"; preload only if CSS/JS-discoveredlcp.md
LCP image too heavysrcset + accurate sizes, AVIF/WebP, transform at uploadimages.md
Render-blocking CSS, slow FCPPer-template critical CSS inline, rest via media swap; purge unused CSScss.md
Text LCP or CLS from web fontsSelf-host WOFF2, preload 1-2 files, font-display + metric-matched fallbackfonts.md
Poor INP / TBTDefer and split JS, re-trigger or delete tags, paint then yieldToMain()inp.md, javascript.md
Third-party tags (GTM, pixels, chat)Inventory, delete, re-trigger after load/consent, facades; offload lastjavascript.md
CLS from images/embeds/bannerswidth/height everywhere, reserved boxes, overlay bannerscls.md
Repeat views slow, assets re-downloadHashed filenames + max-age=31536000, immutablecaching-cdn.md
Back/forward navigations slowRemove bfcache blockers (unload, needless no-store)caching-cdn.md
Wins keep regressingByte budgets (size-limit) + Lighthouse CI on metric audits + field alertsbudgets-ci.md

Step 5: prove it and lock it

  • Lab: same URL, same throttling, median of at least 5 runs, before and after. Warm static caches first, or you are measuring a cache miss.
  • Field: CrUX moves over 28 days; RUM shows it within days. A fix is done when the field p75 says so.
  • CI: @lhci/cli 0.15 still bundles Lighthouse 12, so assert on metric audits, resource-summary:* and *-insight ids, never on legacy audit ids that Lighthouse 13 removed. See budgets-ci.md.

Craft CMS quick map

LeverMovesTrap
Blitz static cache (server rewrites, warm after deploy)TTFBPages containing ungenerated transform URLs are never cached: set generateTransformsBeforePageLoad
.with() / .eagerly() (Craft 5)TTFB on cache missesBlitz Hints was retired in Blitz 5.10; eager-load by hand
{% cache %}TTFB without BlitzDon't combine with Blitz; not auto-disabled for logged-in users
Transforms: getImg(), ImageOptimize imgTag(), Imager XLCP, CLSGlobal lazy-loading macro hits the hero; ImageOptimize has no AVIF
craft-vite critical CSS + modulepreloadRender delay, FCPCSS is async by default: no critical CSS means FOUC and CLS
SEOmatic script positions, per-template .include(false); Formie JS locationINPTracking scripts render only in live: measure production
Queue worker (runQueueAutomatically off)TTFB, INPThe default runs jobs via an extra request from front-end pages

Full mapping, versions and syntax: craft.md. Craft modelling and Twig in general: craftcms-ops.

Reference map

ReferenceRead when
field-vs-lab.mdChoosing data sources; CrUX limits; RUM with web-vitals; Lighthouse 13 insight ids
lcp.mdLCP fails: subparts, discovery, priority, speculation rules
inp.mdINP or TBT fails: phases, yielding, framework notes
cls.mdCLS fails: session windows, culprits, why field beats lab
images.mdFormats, srcset/sizes, fetchpriority, lazy loading
fonts.mdfont-display, preload, subsetting, fallback metrics
javascript.mdBundle budgets, splitting, GTM/third parties, Partytown-style offload
css.mdRender-blocking CSS, critical CSS, purging
caching-cdn.mdCache-Control policy, HTML at the edge, compression, Early Hints, bfcache
budgets-ci.mdLighthouse CI config/assertions, size-limit, field alerts
craft.mdAny Craft CMS site: Blitz, transforms, craft-vite, SEOmatic, Formie, queue

Scripts

ScriptUse
scripts/triage-vitals.py REPORT.jsonRate a Lighthouse / PSI / CrUX report and route each finding to a reference (Step 3)
scripts/check-web-perf-facts.py --offlineAfter editing thresholds or prose: catalog <-> SKILL.md table consistency; flags FID advice that doesn't say INP replaced it
scripts/check-web-perf-facts.py --liveWeekly freshness: web-vitals' own threshold constants, npm majors, deprecations, LHCI's bundled Lighthouse

Both follow the repo's script contract: --help with examples, --json envelopes, exit 10 for findings, 7 (live only) when a source is unreachable.

Boundaries

NeedUse
An agent-driven lab trace through Chrome DevTools MCP (chrome-devtools-mcp)The third-party cloudflare:web-perf skill, if installed: it runs the audit; this skill judges and fixes it
CPU/memory profiling, load tests, slow queries, backend p99perf-ops
Craft content modelling, Twig, element queriescraftcms-ops
Nginx/Cloudflare configuration syntaxnginx-ops, cloudflare-ops
CI workflow hardeningci-cd-ops
Laravel Mix/Webpack -> Vite, Vue 2 -> 3 on a Craft/Twig sitefrontend-upgrade-ops (other stacks: migrate-ops; steady-state Vue 3: vue-ops)

Gotchas

GotchaFix
Optimising the Lighthouse score instead of the field metricStart from CrUX/RUM; the score is a lab composite
"TBT is fine, so INP is fine"TBT covers load only; INP covers every interaction in the visit
Hero image lazy-loaded by a site-wide image macroEager + fetchpriority="high" as an explicit macro parameter for the first image
Measuring a cold static cacheWarm Blitz/CDN before lab runs, or label the run a cache-miss test
Field didn't move the day after a fix28-day window; check RUM or CrUX History weekly points
Third-party tags invisible in labLab runs consent-free; reproduce with consent accepted and read RUM attribution
@lhci/cli assertions on legacy audit idsLHCI bundles Lighthouse 12; legacy ids vanish on 13. Use metric, resource-summary, *-insight ids
@builder.io/partytown in package.jsonDeprecated; the project is now @qwik.dev/partytown (still beta)
LCP or CLS worse right after a Mix -> Vite cutovercraft-vite loads CSS async by default: add critical CSS or asyncCss = false, and re-check modulepreload (frontend-upgrade-ops cutover checklist). Take the lab baseline on the Mix build first

Signals

GitHub stars
43
Forks
7
Last commit
Sep 2026

ahel review

  • K2info
    exfiltration
  • K2info
    exfiltration (in scripts/triage-vitals.py)
  • K2info
    exfiltration (in references/field-vs-lab.md)

Automated review, not a security audit. Ruleset v1+k2.

Advanced
Item type
skill
Key
web-perf-ops
Source
github.com/0xdarkmatter/claude-mods