diff --git a/skills/webperf-core-web-vitals/SKILL.md b/skills/webperf-core-web-vitals/SKILL.md new file mode 100644 index 000000000..b88883a4d --- /dev/null +++ b/skills/webperf-core-web-vitals/SKILL.md @@ -0,0 +1,239 @@ +--- +name: webperf-core-web-vitals +description: Intelligent Core Web Vitals analysis with automated workflows and decision trees. Measures LCP, CLS, INP with guided debugging that automatically determines follow-up analysis based on results. Includes workflows for LCP deep dive (5 phases), CLS investigation (loading vs interaction), INP debugging (latency breakdown + attribution), and cross-skill integration with loading, interaction, and media skills. Use when the user asks about Core Web Vitals, LCP optimization, layout shifts, or interaction responsiveness. +context: fork +--- + +# WebPerf: Core Web Vitals + +JavaScript snippets for measuring web performance in Chrome DevTools. Execute with `evaluate_script`, capture output with `get_console_message`. + +## Scripts + +- `scripts/CLS.js` — Cumulative Layout Shift (CLS) +- `scripts/INP.js` — Interaction to Next Paint (INP) +- `scripts/LCP-Image-Entropy.js` — LCP Image Entropy +- `scripts/LCP-Sub-Parts.js` — LCP Sub-Parts +- `scripts/LCP-Trail.js` — LCP Trail +- `scripts/LCP-Video-Candidate.js` — LCP Video Candidate +- `scripts/LCP.js` — Largest Contentful Paint (LCP) + +Descriptions, thresholds, and return schemas: `references/snippets.md`, `references/schema.md` + +## Script Execution Patterns + +Scripts fall into two execution patterns: + +### Synchronous (LCP, CLS, LCP-Sub-Parts, LCP-Trail, LCP-Image-Entropy, LCP-Video-Candidate) + +Run via `evaluate_script` and return structured JSON immediately from buffered performance data. The page must have already loaded. + +### Tracking (INP) + +INP requires real user interactions to measure. The workflow is: + +1. Run `INP.js` via `evaluate_script` → returns `{ status: "tracking", getDataFn: "getINP" }` +2. **Tell the user:** "INP tracking is now active. Please interact with the page — click buttons, open menus, fill form fields — then let me know when you're done." +3. Wait for the user to confirm they've interacted. +4. Call `evaluate_script("getINP()")` to collect results. +5. If `getINP()` returns `status: "error"` → the user has not interacted yet. Remind them and wait. +6. For a full breakdown of all interactions, call `evaluate_script("getINPDetails()")` — returns all recorded interactions sorted by duration. + +> The agent cannot interact with the page on behalf of the user for INP measurement. Real user interactions are required. + +## Common Workflows + +### Complete Core Web Vitals Audit + +When the user asks for a comprehensive Core Web Vitals analysis or "audit CWV": + +1. **LCP.js** - Measure Largest Contentful Paint +2. **CLS.js** - Measure Cumulative Layout Shift +3. **INP.js** - Measure Interaction to Next Paint +4. **LCP-Sub-Parts.js** - Break down LCP timing phases +5. **LCP-Trail.js** - Track LCP candidate evolution + +### LCP Deep Dive + +When LCP is slow or the user asks "debug LCP" or "why is LCP slow": + +1. **LCP.js** - Establish baseline LCP value +2. **LCP-Sub-Parts.js** - Break down into TTFB, resource load, render delay +3. **LCP-Trail.js** - Identify all LCP candidates and changes +4. **LCP-Image-Entropy.js** - Check if LCP image has visual complexity issues +5. **LCP-Video-Candidate.js** - Detect if LCP is a video (poster or video element) + +### CLS Investigation + +When layout shifts are detected or the user asks "debug CLS" or "layout shift issues": + +1. **CLS.js** - Measure overall CLS score +2. **Layout-Shift-Loading-and-Interaction.js** (from `webperf-interaction` skill) - Separate loading vs interaction shifts +3. Cross-reference with **webperf-loading** skill: + - Find-Above-The-Fold-Lazy-Loaded-Images.js (lazy images causing shifts) + - Fonts-Preloaded-Loaded-and-used-above-the-fold.js (font swap causing shifts) + +### INP Debugging + +When interactions feel slow or the user asks "debug INP" or "slow interactions": + +1. **INP.js** - Start tracking. Tell the user to interact with the page and confirm when done. +2. Call `getINP()` to collect results once the user confirms. +3. Call `getINPDetails()` to see all interactions ranked by duration. +4. **Interactions.js** (from `webperf-interaction` skill) - List all interactions with timing +5. **Input-Latency-Breakdown.js** (from `webperf-interaction` skill) - Break down input delay, processing, presentation +6. **Long-Animation-Frames.js** (from `webperf-interaction` skill) - Identify blocking animation frames +7. **Long-Animation-Frames-Script-Attribution.js** (from `webperf-interaction` skill) - Find scripts causing delays + +### Video as LCP Investigation + +When LCP is a video element (detected by LCP-Video-Candidate.js): + +1. **LCP-Video-Candidate.js** - Identify video as LCP candidate +2. **Video-Element-Audit.js** (from Media skill) - Audit video loading strategy +3. **LCP-Sub-Parts.js** - Analyze video loading phases +4. Cross-reference with **webperf-loading** skill: + - Resource-Hints-Validation.js (check for video preload) + - Priority-Hints-Audit.js (check for fetchpriority on video) + +### Image as LCP Investigation + +When LCP is an image (most common case): + +1. **LCP.js** - Measure LCP timing +2. **LCP-Sub-Parts.js** - Break down timing phases +3. **LCP-Image-Entropy.js** - Analyze image complexity +4. Cross-reference with **webperf-media** skill: + - Image-Element-Audit.js (check format, dimensions, lazy loading) +5. Cross-reference with **webperf-loading** skill: + - Find-Above-The-Fold-Lazy-Loaded-Images.js (check if incorrectly lazy) + - Priority-Hints-Audit.js (check for fetchpriority="high") + - Resource-Hints-Validation.js (check for preload) + +## Decision Tree + +Use this decision tree to automatically run follow-up snippets based on results: + +### After LCP.js + +- **If LCP > 2.5s** → Run **LCP-Sub-Parts.js** to diagnose which phase is slow +- **If LCP > 4.0s (poor)** → Run full LCP deep dive workflow (5 snippets) +- **If LCP candidate is an image** → Run **LCP-Image-Entropy.js** and **webperf-media:Image-Element-Audit.js** +- **If LCP candidate is a video** → Run **LCP-Video-Candidate.js** and **webperf-media:Video-Element-Audit.js** +- **Always run** → **LCP-Trail.js** to understand candidate evolution + +### After LCP-Sub-Parts.js + +- **If TTFB phase > 600ms** → Switch to **webperf-loading** skill and run TTFB-Sub-Parts.js +- **If Resource Load Time > 1500ms** → Run: + 1. **webperf-loading:Resource-Hints-Validation.js** (check for preload/preconnect) + 2. **webperf-loading:Priority-Hints-Audit.js** (check fetchpriority) + 3. **webperf-loading:Find-render-blocking-resources.js** (competing resources) +- **If Render Delay > 200ms** → Run: + 1. **webperf-loading:Find-render-blocking-resources.js** (blocking CSS/JS) + 2. **webperf-loading:Script-Loading.js** (parser-blocking scripts) + 3. **webperf-interaction:Long-Animation-Frames.js** (main thread blocking) + +### After LCP-Trail.js + +- **If many LCP candidate changes (>3)** → This causes visual instability, investigate: + 1. **webperf-loading:Find-Above-The-Fold-Lazy-Loaded-Images.js** (late-loading images) + 2. **webperf-loading:Fonts-Preloaded-Loaded-and-used-above-the-fold.js** (font swaps) + 3. **CLS.js** (layout shifts contributing to LCP changes) +- **If final LCP candidate appears late** → Run **webperf-loading:Resource-Hints-Validation.js** +- **If early candidate was replaced** → Understand why initial content was pushed down (likely CLS issue) + +### After LCP-Image-Entropy.js + +- **If entropy is very high** → Image is visually complex, recommend: + - Modern formats (WebP, AVIF) + - Appropriate compression + - Potentially a placeholder strategy +- **If entropy is low** → Image may be over-optimized or placeholder-like +- **If large file size detected** → Run **webperf-media:Image-Element-Audit.js** for format/sizing analysis + +### After LCP-Video-Candidate.js + +- **If video is LCP** → Run: + 1. **webperf-media:Video-Element-Audit.js** (check poster, preload, formats) + 2. **webperf-loading:Priority-Hints-Audit.js** (check fetchpriority on poster) + 3. **LCP-Sub-Parts.js** (analyze video loading phases) +- **If poster image is LCP** → Treat as image LCP (run image workflows) + +### After CLS.js + +- **If CLS > 0.1** → Run **webperf-interaction:Layout-Shift-Loading-and-Interaction.js** to separate causes +- **If CLS > 0.25 (poor)** → Run comprehensive shift investigation: + 1. **webperf-loading:Find-Above-The-Fold-Lazy-Loaded-Images.js** (images without dimensions) + 2. **webperf-loading:Fonts-Preloaded-Loaded-and-used-above-the-fold.js** (font loading strategy) + 3. **webperf-loading:Critical-CSS-Detection.js** (late-loading styles) + 4. **webperf-media:Image-Element-Audit.js** (missing width/height) +- **If CLS = 0** → Confirm with multiple page loads (might be timing-dependent) + +### After INP.js + +- **If INP > 200ms** → Run **webperf-interaction:Interactions.js** to identify slow interactions +- **If INP > 500ms (poor)** → Run full INP debugging workflow: + 1. **webperf-interaction:Interactions.js** (list all interactions) + 2. **webperf-interaction:Input-Latency-Breakdown.js** (phase breakdown) + 3. **webperf-interaction:Long-Animation-Frames.js** (blocking frames) + 4. **webperf-interaction:Long-Animation-Frames-Script-Attribution.js** (culprit scripts) +- **If specific interaction type is slow (e.g., keyboard)** → Focus analysis on that interaction type + +### Cross-Skill Triggers + +These triggers recommend using snippets from other skills: + +#### From LCP to Loading Skill + +- **If LCP > 2.5s and TTFB phase is dominant** → Use **webperf-loading** skill: + - TTFB.js, TTFB-Sub-Parts.js, Service-Worker-Analysis.js + +- **If LCP image is lazy-loaded** → Use **webperf-loading** skill: + - Find-Above-The-Fold-Lazy-Loaded-Images.js + +- **If LCP has no fetchpriority** → Use **webperf-loading** skill: + - Priority-Hints-Audit.js + +#### From CLS to Loading Skill + +- **If CLS caused by fonts** → Use **webperf-loading** skill: + - Fonts-Preloaded-Loaded-and-used-above-the-fold.js + - Resource-Hints-Validation.js (for font preload) + +- **If CLS caused by images** → Use **webperf-media** skill: + - Image-Element-Audit.js (check for width/height attributes) + +#### From INP to Interaction Skill + +- **If INP > 200ms** → Use **webperf-interaction** skill for full debugging: + - Interactions.js, Input-Latency-Breakdown.js + - Long-Animation-Frames.js, Long-Animation-Frames-Script-Attribution.js + - LongTask.js (if pre-interaction blocking suspected) + +#### From LCP/INP to Interaction Skill + +- **If render delay or interaction delay is high** → Use **webperf-interaction** skill: + - Long-Animation-Frames.js (main thread blocking) + +> **Note on cross-skill references:** This skill runs in an isolated subagent (`context: fork`). When a decision tree recommends scripts from another skill (e.g., `webperf-loading`, `webperf-interaction`, `webperf-media`), report the recommendation to the user as a next step — do not attempt to execute those scripts directly. The user or the main agent can activate the appropriate skill to continue the investigation. + +## Error Recovery + +When a script returns `status: "error"`: + +- **LCP/CLS/LCP-Sub-Parts/LCP-Trail** → The page may not have finished loading. Ask the user to wait for full load or reload, then re-run the script. +- **INP** (`getINP()` returns error) → No interactions have been recorded yet. Remind the user to interact with the page, then call `getINP()` again. +- **LCP-Image-Entropy** → No images with measurable BPP found. This is normal for text-only pages or pages where all images are data URIs. +- **LCP-Video-Candidate** → No LCP entries found; see LCP error recovery above. + +## Visual Highlighting + +By default, scripts highlight the LCP element(s) with colored dashed outlines — useful when the user is watching the browser while the agent runs. To disable: + +```js +window.__cwvHighlight = false; +// then run any LCP script +``` + +Scripts that support this flag: `LCP.js`, `LCP-Sub-Parts.js`, `LCP-Trail.js`. diff --git a/skills/webperf-core-web-vitals/references/schema.md b/skills/webperf-core-web-vitals/references/schema.md new file mode 100644 index 000000000..9fb1f7bd7 --- /dev/null +++ b/skills/webperf-core-web-vitals/references/schema.md @@ -0,0 +1,174 @@ +# Script Return Value Schema + +All scripts return a structured JSON object as the IIFE return value. This allows agents using `evaluate_script` to read structured data directly from the return value, rather than parsing human-readable console output. + +## Base Shape + +```typescript +{ + script: string; // Script name, e.g. "LCP", "CLS", "INP" + status: "ok" // Script ran, has data + | "tracking" // Observer active, data accumulates over time + | "error" // Failed or no data available + | "unsupported"; // Browser API not supported + + // Metric scripts (LCP, CLS, INP) + metric?: string; + value?: number; // Always a number, never a formatted string + unit?: "ms" | "score" | "count" | "bytes" | "bpp" | "fps"; + rating?: "good" | "needs-improvement" | "poor"; + thresholds?: { good: number; needsImprovement: number }; + + // Audit scripts + count?: number; + items?: object[]; + details?: object; + issues?: Array<{ severity: "error" | "warning" | "info"; message: string }>; + + // Tracking scripts + message?: string; + getDataFn?: string; // window function name: evaluate_script(`${getDataFn}()`) + + // Error info + error?: string; +} +``` + +## Agent Workflow + +``` +// Synchronous scripts (LCP, CLS, LCP-Sub-Parts, LCP-Trail, LCP-Image-Entropy, LCP-Video-Candidate) +result = evaluate_script(scriptCode) +// → { status: "ok", value: 1240, rating: "good", ... } + +// Tracking scripts (INP) +result = evaluate_script(INP_js) +// → { status: "tracking", getDataFn: "getINP" } +// (user interacts with the page) +data = evaluate_script("getINP()") +// → { status: "ok", value: 350, rating: "needs-improvement", ... } + +// CLS (hybrid: returns current value immediately, keeps tracking) +result = evaluate_script(CLS_js) +// → { status: "ok", value: 0.05, rating: "good", message: "Call getCLS() for updated value" } +// (after more page interactions) +data = evaluate_script("getCLS()") +// → { status: "ok", value: 0.08, rating: "good", ... } +``` + +## Making Decisions from Return Values + +- `rating === "good"` → no action needed for this metric +- `rating === "needs-improvement"` → investigate, check `details` and `issues` +- `rating === "poor"` → high priority fix, check `issues` for specific problems +- `status === "error"` → page may not have loaded yet, or metric has no data +- `status === "tracking"` → call `evaluate_script(result.getDataFn + "()")` after interaction + +## Script-Specific Schemas + +### LCP +```json +{ + "script": "LCP", "status": "ok", "metric": "LCP", + "value": 1240, "unit": "ms", "rating": "good", + "thresholds": { "good": 2500, "needsImprovement": 4000 }, + "details": { "element": "img.hero", "elementType": "Image", "url": "hero.jpg", "sizePixels": 756000 } +} +``` + +### CLS +```json +{ + "script": "CLS", "status": "ok", "metric": "CLS", + "value": 0.05, "unit": "score", "rating": "good", + "thresholds": { "good": 0.1, "needsImprovement": 0.25 }, + "message": "CLS tracking active. Call getCLS() for updated value after page interactions." +} +``` + +### INP (initial — tracking) +```json +{ "script": "INP", "status": "tracking", "message": "INP tracking active. Interact with the page then call getINP() for results.", "getDataFn": "getINP" } +``` +`getINP()` returns (after interactions): +```json +{ + "script": "INP", "status": "ok", "metric": "INP", + "value": 350, "unit": "ms", "rating": "needs-improvement", + "thresholds": { "good": 200, "needsImprovement": 500 }, + "details": { "totalInteractions": 5, "worstEvent": "click → button.submit", "phases": { "inputDelay": 120, "processingTime": 180, "presentationDelay": 50 } } +} +``` +`getINP()` returns (no interactions yet): +```json +{ "script": "INP", "status": "error", "error": "No interactions recorded yet. Interact with the page and call getINP() again.", "getDataFn": "getINP" } +``` +`getINPDetails()` returns all recorded interactions sorted by duration (useful for INP deep-dive): +```json +[ + { "formattedName": "click → button.submit", "duration": 350, "startTime": 4210, "phases": { "inputDelay": 120, "processingTime": 180, "presentationDelay": 50 } }, + { "formattedName": "keydown → input#search", "duration": 180, "startTime": 8540, "phases": { "inputDelay": 20, "processingTime": 140, "presentationDelay": 20 } } +] +``` + +### LCP-Sub-Parts +```json +{ + "script": "LCP-Sub-Parts", "status": "ok", "metric": "LCP", + "value": 2100, "unit": "ms", "rating": "needs-improvement", + "thresholds": { "good": 2500, "needsImprovement": 4000 }, + "details": { + "element": "img.hero", "url": "hero.jpg", + "subParts": { + "ttfb": { "value": 450, "percent": 21, "overTarget": false }, + "resourceLoadDelay": { "value": 120, "percent": 6, "overTarget": false }, + "resourceLoadTime": { "value": 1200, "percent": 57, "overTarget": true }, + "elementRenderDelay": { "value": 330, "percent": 16, "overTarget": true } + }, + "slowestPhase": "resourceLoadTime" + } +} +``` + +### LCP-Trail +```json +{ + "script": "LCP-Trail", "status": "ok", "metric": "LCP", + "value": 1240, "unit": "ms", "rating": "good", + "thresholds": { "good": 2500, "needsImprovement": 4000 }, + "details": { + "candidateCount": 2, "finalElement": "img.hero", + "candidates": [ + { "index": 1, "selector": "h1", "time": 800, "elementType": "Text block" }, + { "index": 2, "selector": "img.hero", "time": 1240, "elementType": "Image", "url": "hero.jpg" } + ] + } +} +``` + +### LCP-Image-Entropy +```json +{ + "script": "LCP-Image-Entropy", "status": "ok", "count": 5, + "details": { "totalImages": 5, "lowEntropyCount": 1, "lcpImageEligible": true, "lcpImage": { "url": "hero.jpg", "bpp": 1.65, "isLowEntropy": false } }, + "items": [ + { "url": "hero.jpg", "width": 1200, "height": 630, "fileSizeBytes": 156000, "bpp": 1.65, "isLowEntropy": false, "lcpEligible": true, "isLCP": true } + ], + "issues": [] +} +``` + +### LCP-Video-Candidate +```json +{ + "script": "LCP-Video-Candidate", "status": "ok", "metric": "LCP", + "value": 1800, "unit": "ms", "rating": "good", + "thresholds": { "good": 2500, "needsImprovement": 4000 }, + "details": { + "isVideo": true, "posterUrl": "https://example.com/hero.avif", "posterFormat": "avif", + "posterPreloaded": true, "fetchpriorityOnPreload": "high", "isCrossOrigin": false, + "videoAttributes": { "autoplay": true, "muted": true, "playsinline": true, "preload": "auto" } + }, + "issues": [] +} +``` diff --git a/skills/webperf-core-web-vitals/references/snippets.md b/skills/webperf-core-web-vitals/references/snippets.md new file mode 100644 index 000000000..f8c4f3450 --- /dev/null +++ b/skills/webperf-core-web-vitals/references/snippets.md @@ -0,0 +1,95 @@ +--- +## Largest Contentful Paint (LCP) + +Quick check for Largest Contentful Paint, a Core Web Vital that measures loading performance. LCP marks when the largest content element becomes visible in the viewport. + +**Script:** `scripts/LCP.js` + +**Thresholds:** + +| Rating | Time | Meaning | +|--------|------|---------| +| 🟢 Good | ≤ 2.5s | Fast, content appears quickly | +| 🟡 Needs Improvement | ≤ 4s | Moderate delay | +| 🔴 Poor | > 4s | Slow, users may abandon | +--- +## Cumulative Layout Shift (CLS) + +Quick check for Cumulative Layout Shift, a Core Web Vital that measures visual stability. CLS tracks how much the page layout shifts unexpectedly during its lifetime, providing a single score that represents the cumulative impact of all unexpected layout shifts. + +**Script:** `scripts/CLS.js` + +**Usage:** Run `CLS.js` once on page load. It returns the current score immediately and keeps tracking. Call `getCLS()` later to get an updated value after further page interactions. + +**Thresholds:** + +| Rating | Score | Meaning | +|--------|-------|---------| +| 🟢 Good | ≤ 0.1 | Stable, minimal shifting | +| 🟡 Needs Improvement | ≤ 0.25 | Noticeable shifting | +| 🔴 Poor | > 0.25 | Significant layout instability | +--- +## Interaction to Next Paint (INP) + +Tracks Interaction to Next Paint, a Core Web Vital that measures responsiveness. INP evaluates how quickly a page responds to user interactions throughout the entire page visit, replacing First Input Delay (FID) as a Core Web Vital in March 2024. + +**Script:** `scripts/INP.js` + +**Usage:** Run `INP.js` once to start tracking. It returns `status: "tracking"` immediately. After the user interacts with the page, call `getINP()` to retrieve the current INP value. + +**Thresholds:** + +| Rating | Time | Meaning | +|--------|------|---------| +| 🟢 Good | ≤ 200ms | Responsive, feels instant | +| 🟡 Needs Improvement | ≤ 500ms | Noticeable delay | +| 🔴 Poor | > 500ms | Slow, frustrating experience | +--- +## LCP Sub-Parts + +Breaks down Largest Contentful Paint into its four phases to identify optimization opportunities. Understanding which phase is slowest helps focus optimization efforts where they'll have the most impact. + +**Script:** `scripts/LCP-Sub-Parts.js` + +**Sub-parts:** + +| Phase | Target | Description | +|-------|--------|-------------| +| Time to First Byte (TTFB) | ≤ 800ms | Navigation start → first HTML byte | +| Resource Load Delay | < 10% of LCP | TTFB → browser starts loading LCP resource | +| Resource Load Time | ~40% of LCP | Time to download the LCP resource | +| Element Render Delay | < 10% of LCP | Resource downloaded → LCP element rendered | +--- +## LCP Trail + +Tracks every LCP candidate element during page load and highlights each one with a distinct colored dashed outline — so you can see the full trail from first candidate to final LCP. + +**Script:** `scripts/LCP-Trail.js` + +**Returns:** Array of all LCP candidates in order, with selector, time, element type, and URL (if applicable). The last entry is the final LCP element. +--- +## LCP Image Entropy + +Checks if images qualify as LCP candidates based on their entropy (bits per pixel). Since Chrome 112, low-entropy images are ignored for LCP measurement. + +**Script:** `scripts/LCP-Image-Entropy.js` + +**Thresholds:** + +| BPP | Entropy | LCP Eligible | Example | +|-----|---------|--------------|---------| +| < 0.05 | 🔴 Low | ❌ No | Solid colors, simple gradients, placeholders | +| ≥ 0.05 | 🟢 Normal | ✅ Yes | Photos, complex graphics | +--- +## LCP Video Candidate + +Detects whether the LCP element is a `