Skip to content

Commit a8823bc

Browse files
committed
fix: apply skill-creator review feedback to webperf-core-web-vitals
Scripts: - fix(LCP-Video-Candidate): apply activationStart correction to LCP value (bfcache navigations returned inflated values without this) - fix(LCP): use getComputedStyle for background image detection (inline style check missed CSS-driven backgrounds) - fix(CLS): add message field to synchronous return so agent knows to call getCLS() - fix(INP): remove misleading rating:"good" from no-interactions error case; preserve getDataFn so agent can retry after user interacts - feat(LCP, LCP-Sub-Parts, LCP-Trail): add window.__cwvHighlight flag to allow disabling visual element outlines without changing scripts Documentation: - docs(schema): document getINPDetails() with return schema and example - docs(schema): add INP error case (no interactions) to schema examples - docs(SKILL): add Error Recovery section with per-script guidance - docs(SKILL): add cross-skill fork note — cross-skill triggers are recommendations to report, not direct calls from the forked subagent - docs(SKILL): add Visual Highlighting section documenting __cwvHighlight - docs(SKILL): prefix cross-skill script references with skill name - docs(SKILL): document getINPDetails() step in INP debugging workflow
1 parent 49f3c7c commit a8823bc

8 files changed

Lines changed: 79 additions & 25 deletions

File tree

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

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ INP requires real user interactions to measure. The workflow is:
3636
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."
3737
3. Wait for the user to confirm they've interacted.
3838
4. Call `evaluate_script("getINP()")` to collect results.
39+
5. If `getINP()` returns `status: "error"` → the user has not interacted yet. Remind them and wait.
40+
6. For a full breakdown of all interactions, call `evaluate_script("getINPDetails()")` — returns all recorded interactions sorted by duration.
3941

4042
> The agent cannot interact with the page on behalf of the user for INP measurement. Real user interactions are required.
4143
@@ -66,7 +68,7 @@ When LCP is slow or the user asks "debug LCP" or "why is LCP slow":
6668
When layout shifts are detected or the user asks "debug CLS" or "layout shift issues":
6769

6870
1. **CLS.js** - Measure overall CLS score
69-
2. **Layout-Shift-Loading-and-Interaction.js** (from Interaction skill) - Separate loading vs interaction shifts
71+
2. **Layout-Shift-Loading-and-Interaction.js** (from `webperf-interaction` skill) - Separate loading vs interaction shifts
7072
3. Cross-reference with **webperf-loading** skill:
7173
- Find-Above-The-Fold-Lazy-Loaded-Images.js (lazy images causing shifts)
7274
- Fonts-Preloaded-Loaded-and-used-above-the-fold.js (font swap causing shifts)
@@ -77,10 +79,11 @@ When interactions feel slow or the user asks "debug INP" or "slow interactions":
7779

7880
1. **INP.js** - Start tracking. Tell the user to interact with the page and confirm when done.
7981
2. Call `getINP()` to collect results once the user confirms.
80-
3. **Interactions.js** (from Interaction skill) - List all interactions with timing
81-
4. **Input-Latency-Breakdown.js** (from Interaction skill) - Break down input delay, processing, presentation
82-
5. **Long-Animation-Frames.js** (from Interaction skill) - Identify blocking animation frames
83-
6. **Long-Animation-Frames-Script-Attribution.js** (from Interaction skill) - Find scripts causing delays
82+
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
8487

8588
### Video as LCP Investigation
8689

@@ -212,3 +215,25 @@ These triggers recommend using snippets from other skills:
212215

213216
- **If render delay or interaction delay is high** → Use **webperf-interaction** skill:
214217
- 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.
220+
221+
## Error Recovery
222+
223+
When a script returns `status: "error"`:
224+
225+
- **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.
226+
- **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.
228+
- **LCP-Video-Candidate** → No LCP entries found; see LCP error recovery above.
229+
230+
## Visual Highlighting
231+
232+
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:
233+
234+
```js
235+
window.__cwvHighlight = false;
236+
// then run any LCP script
237+
```
238+
239+
Scripts that support this flag: `LCP.js`, `LCP-Sub-Parts.js`, `LCP-Trail.js`.

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ data = evaluate_script("getCLS()")
9090
```json
9191
{ "script": "INP", "status": "tracking", "message": "INP tracking active. Interact with the page then call getINP() for results.", "getDataFn": "getINP" }
9292
```
93-
`getINP()` returns:
93+
`getINP()` returns (after interactions):
9494
```json
9595
{
9696
"script": "INP", "status": "ok", "metric": "INP",
@@ -99,6 +99,17 @@ data = evaluate_script("getCLS()")
9999
"details": { "totalInteractions": 5, "worstEvent": "click → button.submit", "phases": { "inputDelay": 120, "processingTime": 180, "presentationDelay": 50 } }
100100
}
101101
```
102+
`getINP()` returns (no interactions yet):
103+
```json
104+
{ "script": "INP", "status": "error", "error": "No interactions recorded yet. Interact with the page and call getINP() again.", "getDataFn": "getINP" }
105+
```
106+
`getINPDetails()` returns all recorded interactions sorted by duration (useful for INP deep-dive):
107+
```json
108+
[
109+
{ "formattedName": "click → button.submit", "duration": 350, "startTime": 4210, "phases": { "inputDelay": 120, "processingTime": 180, "presentationDelay": 50 } },
110+
{ "formattedName": "keydown → input#search", "duration": 180, "startTime": 8540, "phases": { "inputDelay": 20, "processingTime": 140, "presentationDelay": 20 } }
111+
]
112+
```
102113

103114
### LCP-Sub-Parts
104115
```json

skills/webperf-core-web-vitals/scripts/CLS.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,6 @@
3939
unit: "score",
4040
rating: valueToRating(clsSync),
4141
thresholds: { good: 0.1, needsImprovement: 0.25 },
42+
message: "CLS tracking active. Call getCLS() for updated value after page interactions.",
4243
};
4344
})();

skills/webperf-core-web-vitals/scripts/INP.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,9 @@
7777
}
7878
if (interactions.length === 0) {
7979
return {
80-
script: "INP", status: "error", error: "No interactions recorded yet",
81-
metric: "INP", value: 0, unit: "ms", rating: "good",
82-
thresholds: { good: 200, needsImprovement: 500 }, details,
80+
script: "INP", status: "error",
81+
error: "No interactions recorded yet. Interact with the page and call getINP() again.",
82+
getDataFn: "getINP",
8383
};
8484
}
8585
return {

skills/webperf-core-web-vitals/scripts/LCP-Sub-Parts.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
(() => {
2+
const HIGHLIGHT = window.__cwvHighlight !== false;
3+
24
const valueToRating = (ms) =>
35
ms <= 2500 ? "good" : ms <= 4000 ? "needs-improvement" : "poor";
46

@@ -44,7 +46,7 @@
4446

4547
const { ttfb, lcpRequestStart, lcpResponseEnd, total } = calcSubParts(lcpEntry, navEntry);
4648

47-
if (lcpEntry.element) {
49+
if (HIGHLIGHT && lcpEntry.element) {
4850
lcpEntry.element.style.outline = "3px dashed lime";
4951
lcpEntry.element.style.outlineOffset = "2px";
5052
}

skills/webperf-core-web-vitals/scripts/LCP-Trail.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
(() => {
2+
const HIGHLIGHT = window.__cwvHighlight !== false;
3+
24
const PALETTE = [
35
{ color: "#EF4444" },
46
{ color: "#F97316" },
@@ -42,8 +44,10 @@
4244
const { element } = entry;
4345
if (!element || seen.has(element)) continue;
4446
const { color } = PALETTE[colorIndex % PALETTE.length];
45-
element.style.outline = `3px dashed ${color}`;
46-
element.style.outlineOffset = "2px";
47+
if (HIGHLIGHT) {
48+
element.style.outline = `3px dashed ${color}`;
49+
element.style.outlineOffset = "2px";
50+
}
4751
seen.add(element);
4852
colorIndex++;
4953
}

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@
1010
const valueToRating = (ms) =>
1111
ms <= 2500 ? "good" : ms <= 4000 ? "needs-improvement" : "poor";
1212

13+
const getActivationStart = () => {
14+
const navEntry = performance.getEntriesByType("navigation")[0];
15+
return navEntry?.activationStart || 0;
16+
};
17+
1318
const detectFormat = (url) => {
1419
if (!url) return "unknown";
1520
const ext = url.toLowerCase().split("?")[0].match(/\.(avif|webp|jxl|png|gif|jpg|jpeg|svg)(?:[?#]|$)/);
@@ -22,14 +27,16 @@
2227
catch { return url; }
2328
};
2429

30+
const activationStart = getActivationStart();
31+
2532
if (!element || element.tagName !== "VIDEO") {
2633
return {
2734
script: "LCP-Video-Candidate",
2835
status: "ok",
2936
metric: "LCP",
30-
value: Math.round(lcp.startTime),
37+
value: Math.round(Math.max(0, lcp.startTime - activationStart)),
3138
unit: "ms",
32-
rating: valueToRating(lcp.startTime),
39+
rating: valueToRating(Math.max(0, lcp.startTime - activationStart)),
3340
thresholds: { good: 2500, needsImprovement: 4000 },
3441
details: { isVideo: false },
3542
issues: [],
@@ -76,13 +83,14 @@
7683
issues.push({ severity: "warning", message: 'preload="none" on a non-autoplay video may delay poster image loading in some browsers' });
7784
}
7885

86+
const lcpValue = Math.round(Math.max(0, lcp.startTime - activationStart));
7987
return {
8088
script: "LCP-Video-Candidate",
8189
status: "ok",
8290
metric: "LCP",
83-
value: Math.round(lcp.startTime),
91+
value: lcpValue,
8492
unit: "ms",
85-
rating: valueToRating(lcp.startTime),
93+
rating: valueToRating(lcpValue),
8694
thresholds: { good: 2500, needsImprovement: 4000 },
8795
details: {
8896
isVideo: true,

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

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
(() => {
2+
const HIGHLIGHT = window.__cwvHighlight !== false;
3+
24
const valueToRating = (ms) =>
35
ms <= 2500 ? "good" : ms <= 4000 ? "needs-improvement" : "poor";
46

@@ -7,16 +9,17 @@
79
return navEntry?.activationStart || 0;
810
};
911

12+
const highlightElement = (element, color = "lime") => {
13+
if (!HIGHLIGHT || !element) return;
14+
element.style.outline = `3px dashed ${color}`;
15+
element.style.outlineOffset = "2px";
16+
};
17+
1018
// Highlight LCP element as candidates update
1119
const observer = new PerformanceObserver((list) => {
1220
const entries = list.getEntries();
1321
const lastEntry = entries[entries.length - 1];
14-
if (!lastEntry) return;
15-
const element = lastEntry.element;
16-
if (element) {
17-
element.style.outline = "3px dashed lime";
18-
element.style.outlineOffset = "2px";
19-
}
22+
if (lastEntry?.element) highlightElement(lastEntry.element);
2023
});
2124

2225
observer.observe({ type: "largest-contentful-paint", buffered: true });
@@ -42,14 +45,14 @@
4245
if (classes) selector = `${el.tagName.toLowerCase()}.${classes}`;
4346
}
4447
const tag = el.tagName.toLowerCase();
48+
const hasCssBackground = window.getComputedStyle(el).backgroundImage !== "none";
4549
elementType =
4650
tag === "img" ? "Image" :
4751
tag === "video" ? "Video poster" :
48-
el.style?.backgroundImage ? "Background image" :
52+
hasCssBackground ? "Background image" :
4953
(tag === "h1" || tag === "p" ? "Text block" : tag);
5054

51-
el.style.outline = "3px dashed lime";
52-
el.style.outlineOffset = "2px";
55+
highlightElement(el);
5356
}
5457

5558
return {

0 commit comments

Comments
 (0)