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.
Files changed (67) hide show
  1. package/README.md +139 -0
  2. package/package.json +54 -0
  3. package/snippets/CoreWebVitals/CLS.js +88 -0
  4. package/snippets/CoreWebVitals/INP.js +432 -0
  5. package/snippets/CoreWebVitals/LCP-Image-Entropy.js +198 -0
  6. package/snippets/CoreWebVitals/LCP-Subparts.js +286 -0
  7. package/snippets/CoreWebVitals/LCP-Trail.js +184 -0
  8. package/snippets/CoreWebVitals/LCP-Video-Candidate.js +228 -0
  9. package/snippets/CoreWebVitals/LCP.js +135 -0
  10. package/snippets/CoreWebVitals/WORKFLOWS.md +203 -0
  11. package/snippets/DevTools-Overrides/Fetch-XHR-Timeline-inject.js +73 -0
  12. package/snippets/DevTools-Overrides/Fetch-XHR-Timeline-read.js +191 -0
  13. package/snippets/Interaction/Forced-Synchronous-Layout.js +286 -0
  14. package/snippets/Interaction/Input-Latency-Breakdown.js +199 -0
  15. package/snippets/Interaction/Interactions.js +220 -0
  16. package/snippets/Interaction/Layout-Shift-Loading-and-Interaction.js +266 -0
  17. package/snippets/Interaction/Long-Animation-Frames-Helpers.js +397 -0
  18. package/snippets/Interaction/Long-Animation-Frames-Script-Attribution.js +137 -0
  19. package/snippets/Interaction/Long-Animation-Frames.js +298 -0
  20. package/snippets/Interaction/LongTask.js +203 -0
  21. package/snippets/Interaction/Scroll-Performance.js +359 -0
  22. package/snippets/Interaction/WORKFLOWS.md +205 -0
  23. package/snippets/Loading/Back-Forward-Cache.js +473 -0
  24. package/snippets/Loading/CSS-Media-Queries-Analysis.js +512 -0
  25. package/snippets/Loading/Cache-Strategy-Analysis.js +808 -0
  26. package/snippets/Loading/Client-Side-Redirect-Detection.js +337 -0
  27. package/snippets/Loading/Content-Visibility.js +352 -0
  28. package/snippets/Loading/Critical-CSS-Detection.js +329 -0
  29. package/snippets/Loading/Event-Processing-Time.js +179 -0
  30. package/snippets/Loading/FCP.js +101 -0
  31. package/snippets/Loading/Find-Above-The-Fold-Lazy-Loaded-Images.js +264 -0
  32. package/snippets/Loading/Find-Images-With-Lazy-and-Fetchpriority.js +155 -0
  33. package/snippets/Loading/Find-non-Lazy-Loaded-Images-outside-of-the-viewport.js +300 -0
  34. package/snippets/Loading/Find-render-blocking-resources.js +161 -0
  35. package/snippets/Loading/First-And-Third-Party-Script-Info.js +269 -0
  36. package/snippets/Loading/First-And-Third-Party-Script-Timings.js +295 -0
  37. package/snippets/Loading/Fonts-Preloaded-Loaded-and-used-above-the-fold.js +317 -0
  38. package/snippets/Loading/Inline-CSS-Info-and-Size.js +234 -0
  39. package/snippets/Loading/Inline-Script-Info-and-Size.js +323 -0
  40. package/snippets/Loading/JS-Execution-Time-Breakdown.js +336 -0
  41. package/snippets/Loading/Prefetch-Resource-Validation.js +456 -0
  42. package/snippets/Loading/Priority-Hints-Audit.js +336 -0
  43. package/snippets/Loading/Resource-Hints-Validation.js +503 -0
  44. package/snippets/Loading/Resource-Hints.js +400 -0
  45. package/snippets/Loading/SSR-Hydration-Data-Analysis.js +364 -0
  46. package/snippets/Loading/Script-Loading.js +332 -0
  47. package/snippets/Loading/Service-Worker-Analysis.js +192 -0
  48. package/snippets/Loading/TTFB-Resources.js +115 -0
  49. package/snippets/Loading/TTFB-Sub-Parts.js +117 -0
  50. package/snippets/Loading/TTFB.js +37 -0
  51. package/snippets/Loading/Validate-Preload-Async-Defer-Scripts.js +394 -0
  52. package/snippets/Loading/WORKFLOWS.md +163 -0
  53. package/snippets/Media/Image-Element-Audit.js +350 -0
  54. package/snippets/Media/SVG-Embedded-Bitmap-Analysis.js +225 -0
  55. package/snippets/Media/Video-Element-Audit.js +282 -0
  56. package/snippets/Media/WORKFLOWS.md +190 -0
  57. package/snippets/README.md +98 -0
  58. package/snippets/Resources/Network-Bandwidth-Connection-Quality.js +103 -0
  59. package/snippets/Resources/WORKFLOWS.md +173 -0
  60. package/src/bin.js +229 -0
  61. package/src/decision-tree.js +19 -0
  62. package/src/load-snippet.js +23 -0
  63. package/src/reporters/human.js +179 -0
  64. package/src/reporters/json.js +3 -0
  65. package/src/runner.js +61 -0
  66. package/src/workflows/audit.js +21 -0
  67. 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
+ })();