webperf-snippets 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +139 -0
- package/package.json +54 -0
- package/snippets/CoreWebVitals/CLS.js +88 -0
- package/snippets/CoreWebVitals/INP.js +432 -0
- package/snippets/CoreWebVitals/LCP-Image-Entropy.js +198 -0
- package/snippets/CoreWebVitals/LCP-Subparts.js +286 -0
- package/snippets/CoreWebVitals/LCP-Trail.js +184 -0
- package/snippets/CoreWebVitals/LCP-Video-Candidate.js +228 -0
- package/snippets/CoreWebVitals/LCP.js +135 -0
- package/snippets/CoreWebVitals/WORKFLOWS.md +203 -0
- package/snippets/DevTools-Overrides/Fetch-XHR-Timeline-inject.js +73 -0
- package/snippets/DevTools-Overrides/Fetch-XHR-Timeline-read.js +191 -0
- package/snippets/Interaction/Forced-Synchronous-Layout.js +286 -0
- package/snippets/Interaction/Input-Latency-Breakdown.js +199 -0
- package/snippets/Interaction/Interactions.js +220 -0
- package/snippets/Interaction/Layout-Shift-Loading-and-Interaction.js +266 -0
- package/snippets/Interaction/Long-Animation-Frames-Helpers.js +397 -0
- package/snippets/Interaction/Long-Animation-Frames-Script-Attribution.js +137 -0
- package/snippets/Interaction/Long-Animation-Frames.js +298 -0
- package/snippets/Interaction/LongTask.js +203 -0
- package/snippets/Interaction/Scroll-Performance.js +359 -0
- package/snippets/Interaction/WORKFLOWS.md +205 -0
- package/snippets/Loading/Back-Forward-Cache.js +473 -0
- package/snippets/Loading/CSS-Media-Queries-Analysis.js +512 -0
- package/snippets/Loading/Cache-Strategy-Analysis.js +808 -0
- package/snippets/Loading/Client-Side-Redirect-Detection.js +337 -0
- package/snippets/Loading/Content-Visibility.js +352 -0
- package/snippets/Loading/Critical-CSS-Detection.js +329 -0
- package/snippets/Loading/Event-Processing-Time.js +179 -0
- package/snippets/Loading/FCP.js +101 -0
- package/snippets/Loading/Find-Above-The-Fold-Lazy-Loaded-Images.js +264 -0
- package/snippets/Loading/Find-Images-With-Lazy-and-Fetchpriority.js +155 -0
- package/snippets/Loading/Find-non-Lazy-Loaded-Images-outside-of-the-viewport.js +300 -0
- package/snippets/Loading/Find-render-blocking-resources.js +161 -0
- package/snippets/Loading/First-And-Third-Party-Script-Info.js +269 -0
- package/snippets/Loading/First-And-Third-Party-Script-Timings.js +295 -0
- package/snippets/Loading/Fonts-Preloaded-Loaded-and-used-above-the-fold.js +317 -0
- package/snippets/Loading/Inline-CSS-Info-and-Size.js +234 -0
- package/snippets/Loading/Inline-Script-Info-and-Size.js +323 -0
- package/snippets/Loading/JS-Execution-Time-Breakdown.js +336 -0
- package/snippets/Loading/Prefetch-Resource-Validation.js +456 -0
- package/snippets/Loading/Priority-Hints-Audit.js +336 -0
- package/snippets/Loading/Resource-Hints-Validation.js +503 -0
- package/snippets/Loading/Resource-Hints.js +400 -0
- package/snippets/Loading/SSR-Hydration-Data-Analysis.js +364 -0
- package/snippets/Loading/Script-Loading.js +332 -0
- package/snippets/Loading/Service-Worker-Analysis.js +192 -0
- package/snippets/Loading/TTFB-Resources.js +115 -0
- package/snippets/Loading/TTFB-Sub-Parts.js +117 -0
- package/snippets/Loading/TTFB.js +37 -0
- package/snippets/Loading/Validate-Preload-Async-Defer-Scripts.js +394 -0
- package/snippets/Loading/WORKFLOWS.md +163 -0
- package/snippets/Media/Image-Element-Audit.js +350 -0
- package/snippets/Media/SVG-Embedded-Bitmap-Analysis.js +225 -0
- package/snippets/Media/Video-Element-Audit.js +282 -0
- package/snippets/Media/WORKFLOWS.md +190 -0
- package/snippets/README.md +98 -0
- package/snippets/Resources/Network-Bandwidth-Connection-Quality.js +103 -0
- package/snippets/Resources/WORKFLOWS.md +173 -0
- package/src/bin.js +229 -0
- package/src/decision-tree.js +19 -0
- package/src/load-snippet.js +23 -0
- package/src/reporters/human.js +179 -0
- package/src/reporters/json.js +3 -0
- package/src/runner.js +61 -0
- package/src/workflows/audit.js +21 -0
- package/src/workflows/cwv.js +10 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// LCP Quick Check
|
|
2
|
+
// https://webperf-snippets.nucliweb.net
|
|
3
|
+
|
|
4
|
+
(async () => {
|
|
5
|
+
const valueToRating = (ms) =>
|
|
6
|
+
ms <= 2500 ? "good" : ms <= 4000 ? "needs-improvement" : "poor";
|
|
7
|
+
|
|
8
|
+
const RATING = {
|
|
9
|
+
good: { icon: "š¢", color: "#0CCE6A" },
|
|
10
|
+
"needs-improvement": { icon: "š”", color: "#FFA400" },
|
|
11
|
+
poor: { icon: "š“", color: "#FF4E42" },
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const getActivationStart = () => {
|
|
15
|
+
const navEntry = performance.getEntriesByType("navigation")[0];
|
|
16
|
+
return navEntry?.activationStart || 0;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
let lastPrintedTime = -1;
|
|
20
|
+
|
|
21
|
+
const printLCP = (entry) => {
|
|
22
|
+
const activationStart = getActivationStart();
|
|
23
|
+
const lcpTime = Math.max(0, entry.startTime - activationStart);
|
|
24
|
+
if (lcpTime === lastPrintedTime) return;
|
|
25
|
+
lastPrintedTime = lcpTime;
|
|
26
|
+
|
|
27
|
+
const rating = valueToRating(lcpTime);
|
|
28
|
+
const { icon, color } = RATING[rating];
|
|
29
|
+
|
|
30
|
+
console.group(`%cLCP: ${icon} ${(lcpTime / 1000).toFixed(2)}s (${rating})`, `color: ${color}; font-weight: bold; font-size: 14px;`);
|
|
31
|
+
|
|
32
|
+
const element = entry.element;
|
|
33
|
+
if (element) {
|
|
34
|
+
console.log("");
|
|
35
|
+
console.log("%cLCP Element:", "font-weight: bold;");
|
|
36
|
+
|
|
37
|
+
let selector = element.tagName.toLowerCase();
|
|
38
|
+
if (element.id) selector = `#${element.id}`;
|
|
39
|
+
else if (element.className && typeof element.className === "string") {
|
|
40
|
+
const classes = element.className.trim().split(/\s+/).slice(0, 2).join(".");
|
|
41
|
+
if (classes) selector = `${element.tagName.toLowerCase()}.${classes}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
console.log(` Element: ${selector}`, element);
|
|
45
|
+
|
|
46
|
+
const tagName = element.tagName.toLowerCase();
|
|
47
|
+
if (tagName === "img") {
|
|
48
|
+
console.log(` Type: Image`);
|
|
49
|
+
console.log(` URL: ${entry.url || element.src}`);
|
|
50
|
+
if (element.naturalWidth) {
|
|
51
|
+
console.log(` Dimensions: ${element.naturalWidth}Ć${element.naturalHeight}`);
|
|
52
|
+
}
|
|
53
|
+
} else if (tagName === "video") {
|
|
54
|
+
console.log(` Type: Video poster`);
|
|
55
|
+
console.log(` URL: ${entry.url || element.poster}`);
|
|
56
|
+
} else if (window.getComputedStyle(element).backgroundImage !== "none") {
|
|
57
|
+
console.log(` Type: Background image`);
|
|
58
|
+
console.log(` URL: ${entry.url}`);
|
|
59
|
+
} else {
|
|
60
|
+
console.log(` Type: ${tagName === "h1" || tagName === "p" ? "Text block" : tagName}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (entry.size) {
|
|
64
|
+
console.log(` Size: ${entry.size.toLocaleString()} px²`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
element.style.outline = "3px dashed lime";
|
|
68
|
+
element.style.outlineOffset = "2px";
|
|
69
|
+
console.log("");
|
|
70
|
+
console.log("%cā Element highlighted with green dashed outline", "color: #22c55e;");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.groupEnd();
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const observer = new PerformanceObserver((list) => {
|
|
77
|
+
const entries = list.getEntries();
|
|
78
|
+
const lastEntry = entries[entries.length - 1];
|
|
79
|
+
if (lastEntry) printLCP(lastEntry);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
observer.observe({ type: "largest-contentful-paint", buffered: true });
|
|
83
|
+
|
|
84
|
+
console.log("%cā±ļø LCP Tracking Active", "font-weight: bold; font-size: 14px;");
|
|
85
|
+
console.log(" LCP may update as larger elements load.");
|
|
86
|
+
|
|
87
|
+
// Return for agent ā collect via buffered observer (getEntriesByType does not
|
|
88
|
+
// expose largest-contentful-paint entries in Chrome without an active observer).
|
|
89
|
+
// 100ms gives the observer time to process buffered entries on busy pages.
|
|
90
|
+
const lastLcpEntry = await new Promise((resolve) => {
|
|
91
|
+
const entries = [];
|
|
92
|
+
const obs = new PerformanceObserver((list) => entries.push(...list.getEntries()));
|
|
93
|
+
obs.observe({ type: "largest-contentful-paint", buffered: true });
|
|
94
|
+
setTimeout(() => { obs.disconnect(); resolve(entries.at(-1) ?? null); }, 100);
|
|
95
|
+
});
|
|
96
|
+
if (!lastLcpEntry) {
|
|
97
|
+
return { script: "LCP", status: "error", error: "No LCP entries buffered" };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
printLCP(lastLcpEntry);
|
|
101
|
+
|
|
102
|
+
const lcpActivationStart = getActivationStart();
|
|
103
|
+
const lcpValue = Math.round(Math.max(0, lastLcpEntry.startTime - lcpActivationStart));
|
|
104
|
+
const lcpRating = valueToRating(lcpValue);
|
|
105
|
+
const lcpEl = lastLcpEntry.element;
|
|
106
|
+
let lcpSelector = null;
|
|
107
|
+
let lcpType = null;
|
|
108
|
+
if (lcpEl) {
|
|
109
|
+
lcpSelector = lcpEl.tagName.toLowerCase();
|
|
110
|
+
if (lcpEl.id) lcpSelector = `#${lcpEl.id}`;
|
|
111
|
+
else if (lcpEl.className && typeof lcpEl.className === "string") {
|
|
112
|
+
const classes = lcpEl.className.trim().split(/\s+/).slice(0, 2).join(".");
|
|
113
|
+
if (classes) lcpSelector = `${lcpEl.tagName.toLowerCase()}.${classes}`;
|
|
114
|
+
}
|
|
115
|
+
const tag = lcpEl.tagName.toLowerCase();
|
|
116
|
+
lcpType = tag === "img" ? "Image" : tag === "video" ? "Video poster" :
|
|
117
|
+
window.getComputedStyle(lcpEl).backgroundImage !== "none" ? "Background image" :
|
|
118
|
+
(tag === "h1" || tag === "p" ? "Text block" : tag);
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
script: "LCP",
|
|
122
|
+
status: "ok",
|
|
123
|
+
metric: "LCP",
|
|
124
|
+
value: lcpValue,
|
|
125
|
+
unit: "ms",
|
|
126
|
+
rating: lcpRating,
|
|
127
|
+
thresholds: { good: 2500, needsImprovement: 4000 },
|
|
128
|
+
details: {
|
|
129
|
+
element: lcpSelector,
|
|
130
|
+
elementType: lcpType,
|
|
131
|
+
url: lastLcpEntry.url || null,
|
|
132
|
+
sizePixels: lastLcpEntry.size || null,
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
})();
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
## Common Workflows
|
|
2
|
+
|
|
3
|
+
### Complete Core Web Vitals Audit
|
|
4
|
+
|
|
5
|
+
When the user asks for a comprehensive Core Web Vitals analysis or "audit CWV":
|
|
6
|
+
|
|
7
|
+
1. **LCP.js** - Measure Largest Contentful Paint
|
|
8
|
+
2. **CLS.js** - Measure Cumulative Layout Shift
|
|
9
|
+
3. **INP.js** - Measure Interaction to Next Paint
|
|
10
|
+
4. **LCP-Subparts.js** - Break down LCP timing phases
|
|
11
|
+
5. **LCP-Trail.js** - Track LCP candidate evolution
|
|
12
|
+
|
|
13
|
+
### LCP Deep Dive
|
|
14
|
+
|
|
15
|
+
When LCP is slow or the user asks "debug LCP" or "why is LCP slow":
|
|
16
|
+
|
|
17
|
+
1. **LCP.js** - Establish baseline LCP value
|
|
18
|
+
2. **LCP-Subparts.js** - Break down into TTFB, resource load, render delay
|
|
19
|
+
3. **LCP-Trail.js** - Identify all LCP candidates and changes
|
|
20
|
+
4. **LCP-Image-Entropy.js** - Check if LCP image has visual complexity issues
|
|
21
|
+
5. **LCP-Video-Candidate.js** - Detect if LCP is a video (poster or video element)
|
|
22
|
+
|
|
23
|
+
### CLS Investigation
|
|
24
|
+
|
|
25
|
+
When layout shifts are detected or the user asks "debug CLS" or "layout shift issues":
|
|
26
|
+
|
|
27
|
+
1. **CLS.js** - Measure overall CLS score
|
|
28
|
+
2. **Layout-Shift-Loading-and-Interaction.js** _(pending ā available in webperf-interaction skill)_
|
|
29
|
+
3. Cross-reference with **webperf-loading** skill:
|
|
30
|
+
- Find-Above-The-Fold-Lazy-Loaded-Images.js (lazy images causing shifts)
|
|
31
|
+
- Fonts-Preloaded-Loaded-and-used-above-the-fold.js (font swap causing shifts)
|
|
32
|
+
|
|
33
|
+
### INP Debugging
|
|
34
|
+
|
|
35
|
+
When interactions feel slow or the user asks "debug INP" or "slow interactions":
|
|
36
|
+
|
|
37
|
+
1. **INP.js** - Measure overall INP value; call `getINP()` after interactions, `getINPDetails()` for full list
|
|
38
|
+
2. **Interactions.js** _(pending ā available in webperf-interaction skill)_
|
|
39
|
+
3. **Input-Latency-Breakdown.js** _(pending ā available in webperf-interaction skill)_
|
|
40
|
+
4. **Long-Animation-Frames.js** _(pending ā available in webperf-interaction skill)_
|
|
41
|
+
5. **Long-Animation-Frames-Script-Attribution.js** _(pending ā available in webperf-interaction skill)_
|
|
42
|
+
|
|
43
|
+
### Video as LCP Investigation
|
|
44
|
+
|
|
45
|
+
When LCP is a video element (detected by LCP-Video-Candidate.js):
|
|
46
|
+
|
|
47
|
+
1. **LCP-Video-Candidate.js** - Identify video as LCP candidate
|
|
48
|
+
2. **Video-Element-Audit.js** (from Media skill) - Audit video loading strategy
|
|
49
|
+
3. **LCP-Subparts.js** - Analyze video loading phases
|
|
50
|
+
4. Cross-reference with **webperf-loading** skill:
|
|
51
|
+
- Resource-Hints-Validation.js (check for video preload)
|
|
52
|
+
- Priority-Hints-Audit.js (check for fetchpriority on video)
|
|
53
|
+
|
|
54
|
+
### Image as LCP Investigation
|
|
55
|
+
|
|
56
|
+
When LCP is an image (most common case):
|
|
57
|
+
|
|
58
|
+
1. **LCP.js** - Measure LCP timing
|
|
59
|
+
2. **LCP-Subparts.js** - Break down timing phases
|
|
60
|
+
3. **LCP-Image-Entropy.js** - Analyze image complexity
|
|
61
|
+
4. Cross-reference with **webperf-media** skill:
|
|
62
|
+
- Image-Element-Audit.js (check format, dimensions, lazy loading)
|
|
63
|
+
5. Cross-reference with **webperf-loading** skill:
|
|
64
|
+
- Find-Above-The-Fold-Lazy-Loaded-Images.js (check if incorrectly lazy)
|
|
65
|
+
- Priority-Hints-Audit.js (check for fetchpriority="high")
|
|
66
|
+
- Resource-Hints-Validation.js (check for preload)
|
|
67
|
+
|
|
68
|
+
## Decision Tree
|
|
69
|
+
|
|
70
|
+
Use this decision tree to automatically run follow-up snippets based on results:
|
|
71
|
+
|
|
72
|
+
### After LCP.js
|
|
73
|
+
|
|
74
|
+
- **If LCP > 2.5s** ā Run **LCP-Subparts.js** to diagnose which phase is slow
|
|
75
|
+
- **If LCP > 4.0s (poor)** ā Run full LCP deep dive workflow (5 snippets)
|
|
76
|
+
- **If LCP candidate is an image** ā Run **LCP-Image-Entropy.js** and **webperf-media:Image-Element-Audit.js**
|
|
77
|
+
- **If LCP candidate is a video** ā Run **LCP-Video-Candidate.js** and **webperf-media:Video-Element-Audit.js**
|
|
78
|
+
- **Always run** ā **LCP-Trail.js** to understand candidate evolution
|
|
79
|
+
|
|
80
|
+
### After LCP-Subparts.js
|
|
81
|
+
|
|
82
|
+
- **If TTFB phase > 600ms** ā Switch to **webperf-loading** skill and run TTFB-Sub-Parts.js
|
|
83
|
+
- **If Resource Load Time > 1500ms** ā Run:
|
|
84
|
+
1. **webperf-loading:Resource-Hints-Validation.js** (check for preload/preconnect)
|
|
85
|
+
2. **webperf-loading:Priority-Hints-Audit.js** (check fetchpriority)
|
|
86
|
+
3. **webperf-loading:Find-render-blocking-resources.js** (competing resources)
|
|
87
|
+
- **If Render Delay > 200ms** ā Run:
|
|
88
|
+
1. **webperf-loading:Find-render-blocking-resources.js** (blocking CSS/JS)
|
|
89
|
+
2. **webperf-loading:Script-Loading.js** (parser-blocking scripts)
|
|
90
|
+
3. **webperf-interaction:Long-Animation-Frames.js** (main thread blocking)
|
|
91
|
+
|
|
92
|
+
### After LCP-Trail.js
|
|
93
|
+
|
|
94
|
+
- **If many LCP candidate changes (>3)** ā This causes visual instability, investigate:
|
|
95
|
+
1. **webperf-loading:Find-Above-The-Fold-Lazy-Loaded-Images.js** (late-loading images)
|
|
96
|
+
2. **webperf-loading:Fonts-Preloaded-Loaded-and-used-above-the-fold.js** (font swaps)
|
|
97
|
+
3. **CLS.js** (layout shifts contributing to LCP changes)
|
|
98
|
+
- **If final LCP candidate appears late** ā Run **webperf-loading:Resource-Hints-Validation.js**
|
|
99
|
+
- **If early candidate was replaced** ā Understand why initial content was pushed down (likely CLS issue)
|
|
100
|
+
|
|
101
|
+
### After LCP-Image-Entropy.js
|
|
102
|
+
|
|
103
|
+
- **If entropy is very high** ā Image is visually complex, recommend:
|
|
104
|
+
- Modern formats (WebP, AVIF)
|
|
105
|
+
- Appropriate compression
|
|
106
|
+
- Potentially a placeholder strategy
|
|
107
|
+
- **If entropy is low** ā Image may be over-optimized or placeholder-like
|
|
108
|
+
- **If large file size detected** ā Run **webperf-media:Image-Element-Audit.js** for format/sizing analysis
|
|
109
|
+
|
|
110
|
+
### After LCP-Video-Candidate.js
|
|
111
|
+
|
|
112
|
+
- **If video is LCP** ā Run:
|
|
113
|
+
1. **webperf-media:Video-Element-Audit.js** (check poster, preload, formats)
|
|
114
|
+
2. **webperf-loading:Priority-Hints-Audit.js** (check fetchpriority on poster)
|
|
115
|
+
3. **LCP-Subparts.js** (analyze video loading phases)
|
|
116
|
+
- **If poster image is LCP** ā Treat as image LCP (run image workflows)
|
|
117
|
+
|
|
118
|
+
### After CLS.js
|
|
119
|
+
|
|
120
|
+
- **If CLS > 0.1** ā Run **webperf-interaction:Layout-Shift-Loading-and-Interaction.js** _(pending ā available in webperf-interaction skill)_
|
|
121
|
+
- **If CLS > 0.25 (poor)** ā Run comprehensive shift investigation:
|
|
122
|
+
1. **webperf-loading:Find-Above-The-Fold-Lazy-Loaded-Images.js** (images without dimensions)
|
|
123
|
+
2. **webperf-loading:Fonts-Preloaded-Loaded-and-used-above-the-fold.js** (font loading strategy)
|
|
124
|
+
3. **webperf-loading:Critical-CSS-Detection.js** (late-loading styles)
|
|
125
|
+
4. **webperf-media:Image-Element-Audit.js** (missing width/height)
|
|
126
|
+
- **If CLS = 0** ā Confirm with multiple page loads (might be timing-dependent)
|
|
127
|
+
|
|
128
|
+
### After INP.js
|
|
129
|
+
|
|
130
|
+
- **If INP > 200ms** ā Run **webperf-interaction:Interactions.js** to identify slow interactions
|
|
131
|
+
- **If INP > 500ms (poor)** ā Run full INP debugging workflow:
|
|
132
|
+
1. **webperf-interaction:Interactions.js** (list all interactions)
|
|
133
|
+
2. **webperf-interaction:Input-Latency-Breakdown.js** (phase breakdown)
|
|
134
|
+
3. **webperf-interaction:Long-Animation-Frames.js** (blocking frames)
|
|
135
|
+
4. **webperf-interaction:Long-Animation-Frames-Script-Attribution.js** (culprit scripts)
|
|
136
|
+
- **If specific interaction type is slow (e.g., keyboard)** ā Focus analysis on that interaction type
|
|
137
|
+
- **After INP data is collected** ā call `getINPDetails()` for the full sorted interaction list (useful for identifying patterns across multiple slow interactions)
|
|
138
|
+
|
|
139
|
+
### Error Recovery
|
|
140
|
+
|
|
141
|
+
- **If any script returns `status: "error"`** ā Check if the page has finished loading:
|
|
142
|
+
- If early in load: wait and re-run the script
|
|
143
|
+
- If page is an SPA: user may need to navigate to the target route first
|
|
144
|
+
- **If LCP.js / LCP-Subparts.js returns `status: "error"`** ā Tell the user: "LCP data is not available yet. Please ensure the page has fully loaded, then run the analysis again."
|
|
145
|
+
- **If INP.js `getINP()` returns `status: "error"`** ā The `getDataFn: "getINP"` field signals the agent can retry after user interaction. Prompt the user to click, type, or scroll, then call `getINP()` again.
|
|
146
|
+
|
|
147
|
+
### Cross-Skill Triggers
|
|
148
|
+
|
|
149
|
+
> **Context fork note:** This skill runs with `context: fork`. Cross-skill triggers below are **recommendations to report back to the parent agent**, not direct calls this subagent can execute. When a cross-skill trigger fires, tell the user which skill and script to run next. Scripts marked _(pending)_ are not yet available ā skip them and note the limitation.
|
|
150
|
+
|
|
151
|
+
These triggers recommend using snippets from other skills:
|
|
152
|
+
|
|
153
|
+
#### From LCP to Loading Skill
|
|
154
|
+
|
|
155
|
+
- **If LCP > 2.5s and TTFB phase is dominant** ā Use **webperf-loading** skill:
|
|
156
|
+
- TTFB.js, TTFB-Sub-Parts.js, Service-Worker-Analysis.js
|
|
157
|
+
|
|
158
|
+
- **If LCP image is lazy-loaded** ā Use **webperf-loading** skill:
|
|
159
|
+
- Find-Above-The-Fold-Lazy-Loaded-Images.js
|
|
160
|
+
|
|
161
|
+
- **If LCP has no fetchpriority** ā Use **webperf-loading** skill:
|
|
162
|
+
- Priority-Hints-Audit.js
|
|
163
|
+
|
|
164
|
+
#### From CLS to Loading Skill
|
|
165
|
+
|
|
166
|
+
- **If CLS caused by fonts** ā Use **webperf-loading** skill:
|
|
167
|
+
- Fonts-Preloaded-Loaded-and-used-above-the-fold.js
|
|
168
|
+
- Resource-Hints-Validation.js (for font preload)
|
|
169
|
+
|
|
170
|
+
- **If CLS caused by images** ā Use **webperf-media** skill:
|
|
171
|
+
- Image-Element-Audit.js (check for width/height attributes)
|
|
172
|
+
|
|
173
|
+
#### From INP to Interaction Skill
|
|
174
|
+
|
|
175
|
+
- **If INP > 200ms** ā Use **webperf-interaction** skill for full debugging:
|
|
176
|
+
- Interactions.js, Input-Latency-Breakdown.js
|
|
177
|
+
- Long-Animation-Frames.js, Long-Animation-Frames-Script-Attribution.js
|
|
178
|
+
- LongTask.js (if pre-interaction blocking suspected)
|
|
179
|
+
|
|
180
|
+
#### From LCP/INP to Interaction Skill
|
|
181
|
+
|
|
182
|
+
- **If render delay or interaction delay is high** ā Use **webperf-interaction** skill:
|
|
183
|
+
- Long-Animation-Frames.js (main thread blocking)
|
|
184
|
+
- LongTask.js (long tasks delaying rendering)
|
|
185
|
+
|
|
186
|
+
### Multi-Metric Correlation
|
|
187
|
+
|
|
188
|
+
When multiple CWV metrics are poor, prioritize investigation:
|
|
189
|
+
|
|
190
|
+
- **If LCP > 2.5s AND CLS > 0.1** ā Likely shared root cause:
|
|
191
|
+
1. Check for late-loading content pushing LCP element
|
|
192
|
+
2. Run LCP-Trail.js to see LCP candidate changes
|
|
193
|
+
3. Run Layout-Shift-Loading-and-Interaction.js to correlate timing
|
|
194
|
+
|
|
195
|
+
- **If LCP > 2.5s AND INP > 200ms** ā Main thread congestion:
|
|
196
|
+
1. Run Long-Animation-Frames.js
|
|
197
|
+
2. Run webperf-loading:Script-Loading.js
|
|
198
|
+
3. Run webperf-loading:JS-Execution-Time-Breakdown.js
|
|
199
|
+
|
|
200
|
+
- **If CLS > 0.1 AND INP > 200ms** ā Layout thrashing or interaction-triggered shifts:
|
|
201
|
+
1. Run Layout-Shift-Loading-and-Interaction.js
|
|
202
|
+
2. Run Interactions.js
|
|
203
|
+
3. Check if shifts occur during/after interactions
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Fetch & XHR Timeline ā Inject via DevTools Overrides
|
|
2
|
+
// https://webperf-snippets.nucliweb.net
|
|
3
|
+
(() => {
|
|
4
|
+
const calls = [];
|
|
5
|
+
|
|
6
|
+
// Intercept fetch
|
|
7
|
+
const origFetch = window.fetch;
|
|
8
|
+
window.fetch = function (...args) {
|
|
9
|
+
const url = typeof args[0] === "string" ? args[0] : args[0]?.url;
|
|
10
|
+
const start = performance.now();
|
|
11
|
+
return origFetch.apply(this, args).then((r) => {
|
|
12
|
+
const end = performance.now();
|
|
13
|
+
calls.push({
|
|
14
|
+
type: "fetch",
|
|
15
|
+
url,
|
|
16
|
+
start: Math.round(start),
|
|
17
|
+
end: Math.round(end),
|
|
18
|
+
duration: Math.round(end - start),
|
|
19
|
+
status: r.status,
|
|
20
|
+
});
|
|
21
|
+
return r;
|
|
22
|
+
}).catch((err) => {
|
|
23
|
+
const end = performance.now();
|
|
24
|
+
calls.push({
|
|
25
|
+
type: "fetch",
|
|
26
|
+
url,
|
|
27
|
+
start: Math.round(start),
|
|
28
|
+
end: Math.round(end),
|
|
29
|
+
duration: Math.round(end - start),
|
|
30
|
+
status: "ERROR",
|
|
31
|
+
error: err.message,
|
|
32
|
+
});
|
|
33
|
+
throw err;
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Intercept XHR
|
|
38
|
+
const origOpen = XMLHttpRequest.prototype.open;
|
|
39
|
+
const origSend = XMLHttpRequest.prototype.send;
|
|
40
|
+
|
|
41
|
+
XMLHttpRequest.prototype.open = function (method, url) {
|
|
42
|
+
this.__url = url;
|
|
43
|
+
this.__method = method;
|
|
44
|
+
return origOpen.apply(this, arguments);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
XMLHttpRequest.prototype.send = function () {
|
|
48
|
+
const start = performance.now();
|
|
49
|
+
this.addEventListener("loadend", () => {
|
|
50
|
+
const end = performance.now();
|
|
51
|
+
calls.push({
|
|
52
|
+
type: "xhr",
|
|
53
|
+
url: this.__url,
|
|
54
|
+
start: Math.round(start),
|
|
55
|
+
end: Math.round(end),
|
|
56
|
+
duration: Math.round(end - start),
|
|
57
|
+
status: this.status || "ERROR",
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
return origSend.apply(this, arguments);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
window.__perfCalls = calls;
|
|
64
|
+
|
|
65
|
+
// Capture LCP for correlation in the read snippet
|
|
66
|
+
try {
|
|
67
|
+
new PerformanceObserver((list) => {
|
|
68
|
+
const entries = list.getEntries();
|
|
69
|
+
const last = entries[entries.length - 1];
|
|
70
|
+
window.__lcpTime = Math.round(last.startTime);
|
|
71
|
+
}).observe({ type: "largest-contentful-paint", buffered: true });
|
|
72
|
+
} catch {}
|
|
73
|
+
})();
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// Fetch & XHR Timeline ā Run in console after page load
|
|
2
|
+
// https://webperf-snippets.nucliweb.net
|
|
3
|
+
|
|
4
|
+
(() => {
|
|
5
|
+
const calls = window.__perfCalls;
|
|
6
|
+
|
|
7
|
+
if (!calls) {
|
|
8
|
+
console.warn(
|
|
9
|
+
"No data found. Make sure the inject snippet is active via DevTools Overrides and reload the page."
|
|
10
|
+
);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const lcpEntries = performance.getEntriesByType("largest-contentful-paint");
|
|
15
|
+
const lcpTime = lcpEntries.length
|
|
16
|
+
? Math.round(lcpEntries[lcpEntries.length - 1].startTime)
|
|
17
|
+
: window.__lcpTime ?? null;
|
|
18
|
+
|
|
19
|
+
const isAuthCall = (url = "") => {
|
|
20
|
+
try {
|
|
21
|
+
const { pathname, searchParams } = new URL(url, location.href);
|
|
22
|
+
const path = pathname.toLowerCase();
|
|
23
|
+
if (/\/(auth|login|logout|token|session|whoami|identity|oauth)/.test(path)) return true;
|
|
24
|
+
if (/(^|\/)me(\/|$)/.test(path)) return true;
|
|
25
|
+
if (searchParams.has("authIndexType") || searchParams.has("authIndexValue")) return true;
|
|
26
|
+
return false;
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const isError = (c) =>
|
|
33
|
+
c.status === "ERROR" || (typeof c.status === "number" && c.status >= 400);
|
|
34
|
+
|
|
35
|
+
const beforeLCP = lcpTime ? calls.filter((c) => c.end <= lcpTime) : [];
|
|
36
|
+
const afterLCP = lcpTime ? calls.filter((c) => c.end > lcpTime) : calls;
|
|
37
|
+
const errors = calls.filter(isError);
|
|
38
|
+
const bootstrapErrors = errors.filter((c) => !lcpTime || c.end <= lcpTime);
|
|
39
|
+
const postLCPErrors = errors.filter((c) => lcpTime && c.end > lcpTime);
|
|
40
|
+
const authCalls = calls.filter((c) => isAuthCall(c.url));
|
|
41
|
+
const authBeforeLCP = lcpTime ? authCalls.filter((c) => c.end <= lcpTime) : [];
|
|
42
|
+
|
|
43
|
+
const row = (c) => ({
|
|
44
|
+
Type: c.type,
|
|
45
|
+
Status: c.status,
|
|
46
|
+
"Start (ms)": c.start,
|
|
47
|
+
"End (ms)": c.end,
|
|
48
|
+
"Duration (ms)": c.duration,
|
|
49
|
+
...(lcpTime ? { "Before LCP": c.end <= lcpTime ? "ā ļø yes" : "no" } : {}),
|
|
50
|
+
URL: c.url?.length > 80 ? "..." + c.url.slice(-77) : c.url,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
console.group(
|
|
54
|
+
"%cš” Fetch & XHR Timeline",
|
|
55
|
+
"font-weight: bold; font-size: 14px;"
|
|
56
|
+
);
|
|
57
|
+
console.log("");
|
|
58
|
+
|
|
59
|
+
if (lcpTime) {
|
|
60
|
+
console.log(`%cLCP: ${lcpTime}ms`, "font-weight: bold;");
|
|
61
|
+
console.log("");
|
|
62
|
+
} else {
|
|
63
|
+
console.warn("LCP timing not available ā before/after LCP analysis skipped. Make sure the inject snippet is active and reload the page.");
|
|
64
|
+
console.log("");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (calls.length === 0) {
|
|
68
|
+
console.log(
|
|
69
|
+
"%cā
No fetch or XHR calls detected during page load",
|
|
70
|
+
"color: #22c55e; font-weight: bold;"
|
|
71
|
+
);
|
|
72
|
+
console.groupEnd();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Summary
|
|
77
|
+
console.log("%cSummary:", "font-weight: bold;");
|
|
78
|
+
console.log(` Total calls: ${calls.length}`);
|
|
79
|
+
if (lcpTime) {
|
|
80
|
+
console.log(
|
|
81
|
+
` ${beforeLCP.length > 0 ? "ā ļø" : "ā
"} Before LCP (critical path candidates): ${beforeLCP.length}`
|
|
82
|
+
);
|
|
83
|
+
console.log(
|
|
84
|
+
` ā
After LCP (confirmed non-blocking): ${afterLCP.length}`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
if (bootstrapErrors.length > 0) {
|
|
88
|
+
console.log(` š“ Bootstrap errors (before LCP): ${bootstrapErrors.length}`);
|
|
89
|
+
}
|
|
90
|
+
if (postLCPErrors.length > 0) {
|
|
91
|
+
console.log(` š“ Errors after LCP: ${postLCPErrors.length}`);
|
|
92
|
+
}
|
|
93
|
+
if (authCalls.length > 0) {
|
|
94
|
+
console.log(
|
|
95
|
+
` š Auth-related calls: ${authCalls.length}` +
|
|
96
|
+
(authBeforeLCP.length > 0 ? ` ā ${authBeforeLCP.length} before LCP ā ļø` : "")
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Case 1 ā API calls blocking LCP
|
|
101
|
+
if (beforeLCP.length > 0) {
|
|
102
|
+
console.log("");
|
|
103
|
+
console.group(
|
|
104
|
+
"%cā ļø Calls completing before LCP ā investigate as critical path blockers",
|
|
105
|
+
"color: #ef4444; font-weight: bold;"
|
|
106
|
+
);
|
|
107
|
+
console.log(
|
|
108
|
+
"%cCalls completing before LCP are candidates for deferral, aggressive caching, or parallelization. If a framework waits for their response before rendering, each one adds directly to LCP.",
|
|
109
|
+
"color: #6b7280;"
|
|
110
|
+
);
|
|
111
|
+
console.table(beforeLCP.map(row));
|
|
112
|
+
console.groupEnd();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Case 2 ā Bootstrap errors (before LCP)
|
|
116
|
+
if (bootstrapErrors.length > 0) {
|
|
117
|
+
console.log("");
|
|
118
|
+
console.group(
|
|
119
|
+
"%cš“ Bootstrap errors ā failed calls before LCP",
|
|
120
|
+
"color: #ef4444; font-weight: bold;"
|
|
121
|
+
);
|
|
122
|
+
console.log(
|
|
123
|
+
"%cA failed auth check or config endpoint during bootstrap can add hundreds of ms before first paint ā even when the error is caught silently.",
|
|
124
|
+
"color: #6b7280;"
|
|
125
|
+
);
|
|
126
|
+
console.table(
|
|
127
|
+
bootstrapErrors.map((c) => ({
|
|
128
|
+
Type: c.type,
|
|
129
|
+
Status: c.status,
|
|
130
|
+
Error: c.error || "-",
|
|
131
|
+
"Start (ms)": c.start,
|
|
132
|
+
"Duration (ms)": c.duration,
|
|
133
|
+
URL: c.url?.length > 80 ? "..." + c.url.slice(-77) : c.url,
|
|
134
|
+
}))
|
|
135
|
+
);
|
|
136
|
+
console.groupEnd();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (postLCPErrors.length > 0) {
|
|
140
|
+
console.log("");
|
|
141
|
+
console.group(
|
|
142
|
+
"%cš“ Failed calls after LCP",
|
|
143
|
+
"color: #ef4444; font-weight: bold;"
|
|
144
|
+
);
|
|
145
|
+
console.table(
|
|
146
|
+
postLCPErrors.map((c) => ({
|
|
147
|
+
Type: c.type,
|
|
148
|
+
Status: c.status,
|
|
149
|
+
Error: c.error || "-",
|
|
150
|
+
"Start (ms)": c.start,
|
|
151
|
+
"Duration (ms)": c.duration,
|
|
152
|
+
URL: c.url?.length > 80 ? "..." + c.url.slice(-77) : c.url,
|
|
153
|
+
}))
|
|
154
|
+
);
|
|
155
|
+
console.groupEnd();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Case 4 ā Unnecessary auth checks
|
|
159
|
+
if (authCalls.length > 0) {
|
|
160
|
+
console.log("");
|
|
161
|
+
console.group(
|
|
162
|
+
"%cš Auth-related calls ā verify these are necessary",
|
|
163
|
+
"color: #f59e0b; font-weight: bold;"
|
|
164
|
+
);
|
|
165
|
+
console.log(
|
|
166
|
+
"%cAuth calls on pages where the user is known to be unauthenticated add latency without value. Cross-reference with your routing guards.",
|
|
167
|
+
"color: #6b7280;"
|
|
168
|
+
);
|
|
169
|
+
console.table(authCalls.map(row));
|
|
170
|
+
console.groupEnd();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Case 3 ā Complete network inventory
|
|
174
|
+
console.log("");
|
|
175
|
+
console.group("%cš Complete network inventory", "font-weight: bold;");
|
|
176
|
+
console.log(
|
|
177
|
+
"%cIncludes cross-origin calls invisible to the Network panel timing data (no Timing-Allow-Origin header).",
|
|
178
|
+
"color: #6b7280;"
|
|
179
|
+
);
|
|
180
|
+
console.table(
|
|
181
|
+
calls
|
|
182
|
+
.sort((a, b) => a.start - b.start)
|
|
183
|
+
.map((c) => ({
|
|
184
|
+
...row(c),
|
|
185
|
+
Auth: isAuthCall(c.url) ? "š" : "-",
|
|
186
|
+
}))
|
|
187
|
+
);
|
|
188
|
+
console.groupEnd();
|
|
189
|
+
|
|
190
|
+
console.groupEnd();
|
|
191
|
+
})();
|