Skip to content

Commit 8d84a7d

Browse files
committed
feat(webperf-core-web-vitals): address review feedback
- Remove LCP-Image-Entropy.js: low-entropy filter has been in Chrome stable since Chrome 112, making the script no longer actionable - Update LCP-Video-Candidate.js: Chrome now considers the first frame of <video> elements as an LCP candidate. Add lcpSource ("poster" | "first-frame" | "unknown") to details, replace the missing-poster error with contextual info/warning based on lcpSource - Remove cross-skill references to skills not yet available (webperf-loading, webperf-interaction, webperf-media). Decision trees now reference only scripts in this skill with general guidance where follow-up is needed
1 parent 860efed commit 8d84a7d

5 files changed

Lines changed: 44 additions & 208 deletions

File tree

skills/webperf-core-web-vitals/SKILL.md

Lines changed: 22 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ JavaScript snippets for measuring web performance in Chrome DevTools. Execute wi
1212

1313
- `scripts/CLS.js` — Cumulative Layout Shift (CLS)
1414
- `scripts/INP.js` — Interaction to Next Paint (INP)
15-
- `scripts/LCP-Image-Entropy.js` — LCP Image Entropy
1615
- `scripts/LCP-Sub-Parts.js` — LCP Sub-Parts
1716
- `scripts/LCP-Trail.js` — LCP Trail
1817
- `scripts/LCP-Video-Candidate.js` — LCP Video Candidate
@@ -24,7 +23,7 @@ Descriptions, thresholds, and return schemas: `references/snippets.md`, `referen
2423

2524
Scripts fall into two execution patterns:
2625

27-
### Synchronous (LCP, CLS, LCP-Sub-Parts, LCP-Trail, LCP-Image-Entropy, LCP-Video-Candidate)
26+
### Synchronous (LCP, CLS, LCP-Sub-Parts, LCP-Trail, LCP-Video-Candidate)
2827

2928
Run via `evaluate_script` and return structured JSON immediately from buffered performance data. The page must have already loaded.
3029

@@ -60,18 +59,14 @@ When LCP is slow or the user asks "debug LCP" or "why is LCP slow":
6059
1. **LCP.js** - Establish baseline LCP value
6160
2. **LCP-Subparts.js** - Break down into TTFB, resource load, render delay
6261
3. **LCP-Trail.js** - Identify all LCP candidates and changes
63-
4. **LCP-Image-Entropy.js** - Check if LCP image has visual complexity issues
64-
5. **LCP-Video-Candidate.js** - Detect if LCP is a video (poster or video element)
62+
4. **LCP-Video-Candidate.js** - Detect if LCP is a video (poster or first frame)
6563

6664
### CLS Investigation
6765

6866
When layout shifts are detected or the user asks "debug CLS" or "layout shift issues":
6967

7068
1. **CLS.js** - Measure overall CLS score
71-
2. **Layout-Shift-Loading-and-Interaction.js** (from `webperf-interaction` skill) - Separate loading vs interaction shifts
72-
3. Cross-reference with **webperf-loading** skill:
73-
- Find-Above-The-Fold-Lazy-Loaded-Images.js (lazy images causing shifts)
74-
- Fonts-Preloaded-Loaded-and-used-above-the-fold.js (font swap causing shifts)
69+
2. Call `getCLS()` after page interactions to capture post-load shifts
7570

7671
### INP Debugging
7772

@@ -80,35 +75,21 @@ When interactions feel slow or the user asks "debug INP" or "slow interactions":
8075
1. **INP.js** - Start measuring. Tell the user to interact with the page and confirm when done.
8176
2. Call `getINP()` to collect results once the user confirms.
8277
3. Call `getINPDetails()` to see all interactions ranked by duration.
83-
4. **Interactions.js** (from `webperf-interaction` skill) - List all interactions with timing
84-
5. **Input-Latency-Breakdown.js** (from `webperf-interaction` skill) - Break down input delay, processing, presentation
85-
6. **Long-Animation-Frames.js** (from `webperf-interaction` skill) - Identify blocking animation frames
86-
7. **Long-Animation-Frames-Script-Attribution.js** (from `webperf-interaction` skill) - Find scripts causing delays
8778

8879
### Video as LCP Investigation
8980

9081
When LCP is a video element (detected by LCP-Video-Candidate.js):
9182

92-
1. **LCP-Video-Candidate.js** - Identify video as LCP candidate
93-
2. **Video-Element-Audit.js** (from Media skill) - Audit video loading strategy
94-
3. **LCP-Subparts.js** - Analyze video loading phases
95-
4. Cross-reference with **webperf-loading** skill:
96-
- Resource-Hints-Validation.js (check for video preload)
97-
- Priority-Hints-Audit.js (check for fetchpriority on video)
83+
1. **LCP-Video-Candidate.js** - Identify video as LCP candidate, detect `lcpSource` (poster or first frame)
84+
2. **LCP-Subparts.js** - Analyze video loading phases
9885

9986
### Image as LCP Investigation
10087

10188
When LCP is an image (most common case):
10289

10390
1. **LCP.js** - Measure LCP timing
10491
2. **LCP-Subparts.js** - Break down timing phases
105-
3. **LCP-Image-Entropy.js** - Analyze image complexity
106-
4. Cross-reference with **webperf-media** skill:
107-
- Image-Element-Audit.js (check format, dimensions, lazy loading)
108-
5. Cross-reference with **webperf-loading** skill:
109-
- Find-Above-The-Fold-Lazy-Loaded-Images.js (check if incorrectly lazy)
110-
- Priority-Hints-Audit.js (check for fetchpriority="high")
111-
- Resource-Hints-Validation.js (check for preload)
92+
3. **LCP-Trail.js** - Track all LCP candidates to confirm final element
11293

11394
## Decision Tree
11495

@@ -117,114 +98,46 @@ Use this decision tree to automatically run follow-up snippets based on results:
11798
### After LCP.js
11899

119100
- **If LCP > 2.5s** → Run **LCP-Sub-Parts.js** to diagnose which phase is slow
120-
- **If LCP > 4.0s (poor)** → Run full LCP deep dive workflow (5 snippets)
121-
- **If LCP candidate is an image** → Run **LCP-Image-Entropy.js** and **webperf-media:Image-Element-Audit.js**
122-
- **If LCP candidate is a video** → Run **LCP-Video-Candidate.js** and **webperf-media:Video-Element-Audit.js**
101+
- **If LCP > 4.0s (poor)** → Run full LCP deep dive workflow
102+
- **If LCP candidate is a video** → Run **LCP-Video-Candidate.js**
123103
- **Always run****LCP-Trail.js** to understand candidate evolution
124104

125105
### After LCP-Subparts.js
126106

127-
- **If TTFB phase > 600ms** → Switch to **webperf-loading** skill and run TTFB-Sub-Parts.js
128-
- **If Resource Load Time > 1500ms** → Run:
129-
1. **webperf-loading:Resource-Hints-Validation.js** (check for preload/preconnect)
130-
2. **webperf-loading:Priority-Hints-Audit.js** (check fetchpriority)
131-
3. **webperf-loading:Find-render-blocking-resources.js** (competing resources)
132-
- **If Render Delay > 200ms** → Run:
133-
1. **webperf-loading:Find-render-blocking-resources.js** (blocking CSS/JS)
134-
2. **webperf-loading:Script-Loading.js** (parser-blocking scripts)
135-
3. **webperf-interaction:Long-Animation-Frames.js** (main thread blocking)
107+
- **If TTFB phase > 600ms** → Investigate server response time and redirects
108+
- **If Resource Load Time > 1500ms** → Check preload hints and fetch priority for the LCP resource
109+
- **If Render Delay > 200ms** → Investigate render-blocking resources and main thread work
136110

137111
### After LCP-Trail.js
138112

139-
- **If many LCP candidate changes (>3)** → This causes visual instability, investigate:
140-
1. **webperf-loading:Find-Above-The-Fold-Lazy-Loaded-Images.js** (late-loading images)
141-
2. **webperf-loading:Fonts-Preloaded-Loaded-and-used-above-the-fold.js** (font swaps)
142-
3. **CLS.js** (layout shifts contributing to LCP changes)
143-
- **If final LCP candidate appears late** → Run **webperf-loading:Resource-Hints-Validation.js**
144-
- **If early candidate was replaced** → Understand why initial content was pushed down (likely CLS issue)
145-
146-
### After LCP-Image-Entropy.js
147-
148-
- **If entropy is very high** → Image is visually complex, recommend:
149-
- Modern formats (WebP, AVIF)
150-
- Appropriate compression
151-
- Potentially a placeholder strategy
152-
- **If entropy is low** → Image may be over-optimized or placeholder-like
153-
- **If large file size detected** → Run **webperf-media:Image-Element-Audit.js** for format/sizing analysis
113+
- **If many LCP candidate changes (>3)** → Visual instability; run **CLS.js** to check layout shifts
114+
- **If final LCP candidate appears late** → Investigate resource preloading for the LCP element
115+
- **If early candidate was replaced** → Likely a CLS issue; run **CLS.js**
154116

155117
### After LCP-Video-Candidate.js
156118

157-
- **If video is LCP** → Run:
158-
1. **webperf-media:Video-Element-Audit.js** (check poster, preload, formats)
159-
2. **webperf-loading:Priority-Hints-Audit.js** (check fetchpriority on poster)
160-
3. **LCP-Sub-Parts.js** (analyze video loading phases)
161-
- **If poster image is LCP** → Treat as image LCP (run image workflows)
119+
- **If `lcpSource === "poster"`** → Check poster preload and `fetchpriority="high"`; run **LCP-Subparts.js**
120+
- **If `lcpSource === "first-frame"`** → Ensure `autoplay` + `muted` + `playsinline` are set; adding a poster gives explicit control
121+
- **If `lcpSource === "unknown"`** → No poster URL or video URL detectable; run **LCP-Subparts.js** for timing breakdown
162122

163123
### After CLS.js
164124

165-
- **If CLS > 0.1** → Run **webperf-interaction:Layout-Shift-Loading-and-Interaction.js** to separate causes
166-
- **If CLS > 0.25 (poor)** → Run comprehensive shift investigation:
167-
1. **webperf-loading:Find-Above-The-Fold-Lazy-Loaded-Images.js** (images without dimensions)
168-
2. **webperf-loading:Fonts-Preloaded-Loaded-and-used-above-the-fold.js** (font loading strategy)
169-
3. **webperf-loading:Critical-CSS-Detection.js** (late-loading styles)
170-
4. **webperf-media:Image-Element-Audit.js** (missing width/height)
125+
- **If CLS > 0.1** → Check `sources` in the result for the shifting elements; inspect for missing `width`/`height` attributes, late-loading fonts, or dynamic content insertion
126+
- **If CLS > 0.25 (poor)** → Call `getCLS()` after interactions to confirm the score accumulates over time
171127
- **If CLS = 0** → Confirm with multiple page loads (might be timing-dependent)
172128

173129
### After INP.js
174130

175-
- **If INP > 200ms** → Run **webperf-interaction:Interactions.js** to identify slow interactions
176-
- **If INP > 500ms (poor)** → Run full INP debugging workflow:
177-
1. **webperf-interaction:Interactions.js** (list all interactions)
178-
2. **webperf-interaction:Input-Latency-Breakdown.js** (phase breakdown)
179-
3. **webperf-interaction:Long-Animation-Frames.js** (blocking frames)
180-
4. **webperf-interaction:Long-Animation-Frames-Script-Attribution.js** (culprit scripts)
181-
- **If specific interaction type is slow (e.g., keyboard)** → Focus analysis on that interaction type
182-
183-
### Cross-Skill Triggers
184-
185-
These triggers recommend using snippets from other skills:
186-
187-
#### From LCP to Loading Skill
188-
189-
- **If LCP > 2.5s and TTFB phase is dominant** → Use **webperf-loading** skill:
190-
- TTFB.js, TTFB-Sub-Parts.js, Service-Worker-Analysis.js
191-
192-
- **If LCP image is lazy-loaded** → Use **webperf-loading** skill:
193-
- Find-Above-The-Fold-Lazy-Loaded-Images.js
194-
195-
- **If LCP has no fetchpriority** → Use **webperf-loading** skill:
196-
- Priority-Hints-Audit.js
197-
198-
#### From CLS to Loading Skill
199-
200-
- **If CLS caused by fonts** → Use **webperf-loading** skill:
201-
- Fonts-Preloaded-Loaded-and-used-above-the-fold.js
202-
- Resource-Hints-Validation.js (for font preload)
203-
204-
- **If CLS caused by images** → Use **webperf-media** skill:
205-
- Image-Element-Audit.js (check for width/height attributes)
206-
207-
#### From INP to Interaction Skill
208-
209-
- **If INP > 200ms** → Use **webperf-interaction** skill for full debugging:
210-
- Interactions.js, Input-Latency-Breakdown.js
211-
- Long-Animation-Frames.js, Long-Animation-Frames-Script-Attribution.js
212-
- LongTask.js (if pre-interaction blocking suspected)
213-
214-
#### From LCP/INP to Interaction Skill
215-
216-
- **If render delay or interaction delay is high** → Use **webperf-interaction** skill:
217-
- Long-Animation-Frames.js (main thread blocking)
218-
219-
> **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.
131+
- **If INP > 200ms** → Call `getINPDetails()` to list all interactions ranked by duration and identify the slowest one
132+
- **If INP > 500ms (poor)** → Check `phases` in the worst interaction: high `inputDelay` suggests main thread blocking; high `processingDuration` suggests heavy event handler work
133+
- **If specific interaction type is slow (e.g., keyboard)** → Focus `getINPDetails()` on that interaction type
220134

221135
## Error Recovery
222136

223137
When a script returns `status: "error"`:
224138

225139
- **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.
226140
- **INP** (`getINP()` returns error) → No interactions have been recorded yet. Remind the user to interact with the page, then call `getINP()` again.
227-
- **LCP-Image-Entropy** → No images with measurable BPP found. This is normal for text-only pages or pages where all images are data URIs.
228141
- **LCP-Video-Candidate** → No LCP entries found; see LCP error recovery above.
229142

230143
## Visual Highlighting

skills/webperf-core-web-vitals/references/schema.md

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ All scripts return a structured JSON object as the return value. This allows age
3737
## Agent Workflow
3838

3939
```
40-
// Synchronous scripts (LCP, CLS, LCP-Subparts, LCP-Trail, LCP-Image-Entropy, LCP-Video-Candidate)
40+
// Synchronous scripts (LCP, CLS, LCP-Subparts, LCP-Trail, LCP-Video-Candidate)
4141
result = evaluate_script(scriptCode)
4242
// → { status: "ok", value: 1240, rating: "good", ... }
4343
@@ -136,26 +136,15 @@ data = evaluate_script("getCLS()")
136136
}
137137
```
138138

139-
### LCP-Image-Entropy
140-
```json
141-
{
142-
"script": "LCP-Image-Entropy", "status": "ok", "count": 5,
143-
"details": { "totalImages": 5, "lowEntropyCount": 1, "lcpImageEligible": true, "lcpImage": { "url": "hero.jpg", "bpp": 1.65, "isLowEntropy": false } },
144-
"items": [
145-
{ "url": "hero.jpg", "width": 1200, "height": 630, "fileSizeBytes": 156000, "bpp": 1.65, "isLowEntropy": false, "lcpEligible": true, "isLCP": true }
146-
],
147-
"issues": []
148-
}
149-
```
150-
151139
### LCP-Video-Candidate
152140
```json
153141
{
154142
"script": "LCP-Video-Candidate", "status": "ok", "metric": "LCP",
155143
"value": 1800, "unit": "ms", "rating": "good",
156144
"thresholds": { "good": 2500, "needsImprovement": 4000 },
157145
"details": {
158-
"isVideo": true, "posterUrl": "https://example.com/hero.avif", "posterFormat": "avif",
146+
"isVideo": true, "lcpSource": "poster",
147+
"posterUrl": "https://example.com/hero.avif", "posterFormat": "avif",
159148
"posterPreloaded": true, "fetchpriorityOnPreload": "high", "isCrossOrigin": false,
160149
"videoAttributes": { "autoplay": true, "muted": true, "playsinline": true, "preload": "auto" }
161150
},

skills/webperf-core-web-vitals/references/snippets.md

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -68,27 +68,15 @@ Tracks every LCP candidate element during page load and highlights each one with
6868

6969
**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.
7070
---
71-
## LCP Image Entropy
72-
73-
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.
74-
75-
**Script:** `scripts/LCP-Image-Entropy.js`
76-
77-
**Thresholds:**
78-
79-
| BPP | Entropy | LCP Eligible | Example |
80-
|-----|---------|--------------|---------|
81-
| < 0.05 | 🔴 Low | ❌ No | Solid colors, simple gradients, placeholders |
82-
| ≥ 0.05 | 🟢 Normal | ✅ Yes | Photos, complex graphics |
83-
---
8471
## LCP Video Candidate
8572

86-
Detects whether the LCP element is a `<video>` and audits the poster image configuration — the most common source of avoidable LCP delay when video is the hero element.
73+
Detects whether the LCP element is a `<video>` and audits the configuration. Chrome considers both the poster image and the first frame of the video as LCP candidates.
8774

8875
**Script:** `scripts/LCP-Video-Candidate.js`
8976

9077
**Checks:**
91-
- Whether a `poster` attribute exists
78+
- Whether the LCP source is the poster image or the first video frame (`lcpSource`)
79+
- Whether a `poster` attribute exists (recommended for explicit control)
9280
- Whether the poster is preloaded with `<link rel="preload" as="image">`
9381
- Whether `fetchpriority="high"` is set on the preload
9482
- Whether the poster uses a modern format (AVIF, WebP)

skills/webperf-core-web-vitals/scripts/LCP-Image-Entropy.js

Lines changed: 0 additions & 67 deletions
This file was deleted.

skills/webperf-core-web-vitals/scripts/LCP-Video-Candidate.js

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,15 @@
4646
const posterAttr = element.getAttribute("poster") || "";
4747
const posterUrl = posterAttr ? normalizeUrl(posterAttr) : "";
4848
const lcpUrl = lcp.url || "";
49+
50+
// Chrome considers both poster image and first video frame as LCP candidates.
51+
// lcpUrl is set when the LCP came from the poster; empty when from the first frame.
52+
const lcpSource = lcpUrl
53+
? "poster"
54+
: posterAttr
55+
? "unknown"
56+
: "first-frame";
57+
4958
const posterFormat = detectFormat(lcpUrl || posterUrl);
5059
const isModernFormat = ["avif", "webp", "jxl"].includes(posterFormat);
5160
const isCrossOrigin = lcp.renderTime === 0 && lcp.loadTime > 0;
@@ -65,8 +74,11 @@
6574
const playsinline = element.hasAttribute("playsinline");
6675

6776
const issues = [];
68-
if (!posterAttr) {
69-
issues.push({ severity: "error", message: "No poster attribute — the browser has no image to use as LCP candidate" });
77+
if (lcpSource === "first-frame") {
78+
issues.push({ severity: "info", message: "LCP is the first video frame — adding a poster gives explicit control over the LCP image" });
79+
}
80+
if (lcpSource === "first-frame" && (!autoplay || !muted)) {
81+
issues.push({ severity: "warning", message: "First-frame LCP requires autoplay + muted for the browser to render it immediately" });
7082
}
7183
if (posterAttr && !posterPreload) {
7284
issues.push({ severity: "warning", message: 'No <link rel="preload" as="image"> for the poster — browser discovers it late' });
@@ -77,7 +89,7 @@
7789
issues.push({ severity: "info", message: `Poster uses ${posterFormat} — AVIF or WebP would reduce file size and LCP load time` });
7890
}
7991
if (isCrossOrigin) {
80-
issues.push({ severity: "info", message: "renderTime is 0 — poster is cross-origin and the server does not send Timing-Allow-Origin" });
92+
issues.push({ severity: "info", message: "renderTime is 0 — resource is cross-origin and the server does not send Timing-Allow-Origin" });
8193
}
8294
if (!autoplay && preload === "none") {
8395
issues.push({ severity: "warning", message: 'preload="none" on a non-autoplay video may delay poster image loading in some browsers' });
@@ -94,6 +106,7 @@
94106
thresholds: { good: 2500, needsImprovement: 4000 },
95107
details: {
96108
isVideo: true,
109+
lcpSource,
97110
posterUrl: lcpUrl || posterUrl || null,
98111
posterFormat,
99112
posterPreloaded: !!posterPreload,

0 commit comments

Comments
 (0)