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,264 @@
1
+ // Detect lazy loaded images above the fold
2
+ // https://webperf-snippets.nucliweb.net
3
+
4
+ (function() {
5
+ function getSelector(node) {
6
+ let sel = "";
7
+ try {
8
+ while (node && node.nodeType !== 9) {
9
+ const el = node;
10
+ const name = el.nodeName.toLowerCase();
11
+ const part = el.id
12
+ ? "#" + el.id
13
+ : name +
14
+ (el.classList && el.classList.value && el.classList.value.trim()
15
+ ? "." + el.classList.value.trim().split(/\s+/).slice(0, 2).join(".")
16
+ : "");
17
+ if (sel.length + part.length > 80) return sel || part;
18
+ sel = sel ? part + ">" + sel : part;
19
+ if (el.id) break;
20
+ node = el.parentNode;
21
+ }
22
+ } catch {}
23
+ return sel;
24
+ }
25
+
26
+ function isInViewport(element) {
27
+ const rect = element.getBoundingClientRect();
28
+ return (
29
+ rect.top < window.innerHeight &&
30
+ rect.bottom > 0 &&
31
+ rect.left < window.innerWidth &&
32
+ rect.right > 0 &&
33
+ rect.width > 0 &&
34
+ rect.height > 0
35
+ );
36
+ }
37
+
38
+ function getDistanceFromViewportEdge(element) {
39
+ const rect = element.getBoundingClientRect();
40
+ const distances = [
41
+ rect.top,
42
+ window.innerHeight - rect.bottom,
43
+ rect.left,
44
+ window.innerWidth - rect.right
45
+ ];
46
+ return Math.min(...distances.map(d => Math.abs(d)));
47
+ }
48
+
49
+ function getFileSize(src) {
50
+ if (!src) return null;
51
+ try {
52
+ const entries = performance.getEntriesByType("resource");
53
+ const entry = entries.find(e => e.name === src || e.name.includes(src.split("/").pop()));
54
+ if (entry && entry.transferSize) {
55
+ return (entry.transferSize / 1024).toFixed(1) + " KB";
56
+ }
57
+ } catch {}
58
+ return null;
59
+ }
60
+
61
+ function getLazyType(element) {
62
+ const hasLoadingLazy = element.getAttribute("loading") === "lazy";
63
+ const hasDataSrc = element.hasAttribute("data-src") || element.hasAttribute("data-lazy");
64
+
65
+ if (hasLoadingLazy && hasDataSrc) return "both";
66
+ if (hasLoadingLazy) return "loading-lazy";
67
+ if (hasDataSrc) return "data-src";
68
+ return null;
69
+ }
70
+
71
+ function getElementInfo(img) {
72
+ const rect = img.getBoundingClientRect();
73
+ const area = rect.width * rect.height;
74
+
75
+ return {
76
+ selector: getSelector(img),
77
+ lazyType: getLazyType(img),
78
+ dimensions: `${Math.round(rect.width)}×${Math.round(rect.height)}`,
79
+ position: {
80
+ top: Math.round(rect.top + window.scrollY),
81
+ left: Math.round(rect.left + window.scrollX)
82
+ },
83
+ distanceFromEdge: Math.round(getDistanceFromViewportEdge(img)) + "px",
84
+ src: img.currentSrc || img.src || img.getAttribute("data-src") || "",
85
+ srcset: img.srcset || null,
86
+ sizes: img.sizes || null,
87
+ alt: img.alt || "(no alt)",
88
+ fetchPriority: img.fetchPriority || "auto",
89
+ fileSize: getFileSize(img.currentSrc || img.src),
90
+ area: area,
91
+ element: img
92
+ };
93
+ }
94
+
95
+ const results = {
96
+ lazyImages: [],
97
+ lcpCandidate: null,
98
+ nodeArray: [],
99
+ summary: {
100
+ total: 0,
101
+ withLoadingLazy: 0,
102
+ withDataSrc: 0,
103
+ lcpAffected: false
104
+ }
105
+ };
106
+
107
+ // Find all images with lazy loading
108
+ const lazySelectors = '[loading="lazy"], [data-src], [data-lazy]';
109
+ const lazyElements = document.querySelectorAll(`img${lazySelectors}, picture source${lazySelectors}`);
110
+
111
+ // Also check all images in viewport to find LCP candidate
112
+ const allViewportImages = Array.from(document.querySelectorAll("img"))
113
+ .filter(img => isInViewport(img) && img.getBoundingClientRect().width > 0);
114
+
115
+ // Find LCP candidate (largest visible image)
116
+ let lcpCandidate = null;
117
+ let maxArea = 0;
118
+
119
+ allViewportImages.forEach(img => {
120
+ const rect = img.getBoundingClientRect();
121
+ const area = rect.width * rect.height;
122
+ if (area > maxArea) {
123
+ maxArea = area;
124
+ lcpCandidate = img;
125
+ }
126
+ });
127
+
128
+ // Process lazy loaded images in viewport
129
+ lazyElements.forEach(element => {
130
+ const img = element.tagName === "SOURCE"
131
+ ? element.closest("picture")?.querySelector("img")
132
+ : element;
133
+
134
+ if (!img || !isInViewport(img)) return;
135
+
136
+ const info = getElementInfo(img);
137
+ info.isLcpCandidate = img === lcpCandidate;
138
+
139
+ results.lazyImages.push(info);
140
+ results.nodeArray.push(img);
141
+
142
+ // Update summary
143
+ results.summary.total++;
144
+ if (info.lazyType === "loading-lazy" || info.lazyType === "both") {
145
+ results.summary.withLoadingLazy++;
146
+ }
147
+ if (info.lazyType === "data-src" || info.lazyType === "both") {
148
+ results.summary.withDataSrc++;
149
+ }
150
+ if (info.isLcpCandidate) {
151
+ results.summary.lcpAffected = true;
152
+ results.lcpCandidate = info;
153
+ }
154
+ });
155
+
156
+ // Display results
157
+ console.group("🔍 Lazy Loading Detection - Above The Fold");
158
+
159
+ if (results.lazyImages.length === 0) {
160
+ console.log(
161
+ "%c✅ Good job! No lazily loaded images found in the viewport.",
162
+ "background: #222; color: #22c55e; padding: 0.5ch 1ch; font-weight: bold;"
163
+ );
164
+ } else {
165
+ // Summary
166
+ console.log(
167
+ `%c⚠️ Found ${results.summary.total} lazily loaded image(s) above the fold`,
168
+ "color: #f59e0b; font-weight: bold; font-size: 14px;"
169
+ );
170
+ console.log("");
171
+
172
+ // LCP Warning
173
+ if (results.summary.lcpAffected) {
174
+ console.group("🚨 Critical: LCP Candidate Has Lazy Loading");
175
+ console.log(
176
+ "%cThe largest image in your viewport has lazy loading enabled!",
177
+ "color: #ef4444; font-weight: bold;"
178
+ );
179
+ console.log("This can significantly delay your Largest Contentful Paint.");
180
+ console.log("");
181
+ console.log("LCP Candidate:", results.lcpCandidate.selector);
182
+ console.log("Dimensions:", results.lcpCandidate.dimensions);
183
+ console.log("File size:", results.lcpCandidate.fileSize || "unknown");
184
+ console.log("");
185
+ console.log("%cFix: Remove loading=\"lazy\" and add fetchpriority=\"high\"", "color: #22c55e;");
186
+ console.groupEnd();
187
+ console.log("");
188
+ }
189
+
190
+ // Table of all problematic images
191
+ console.group("📋 Lazy Loaded Images in Viewport");
192
+ const tableData = results.lazyImages.map((image) => ({
193
+ selector: image.selector,
194
+ lazyType: image.lazyType,
195
+ dimensions: image.dimensions,
196
+ top: image.position.top + "px",
197
+ distanceFromEdge: image.distanceFromEdge,
198
+ src: image.src,
199
+ srcset: image.srcset,
200
+ sizes: image.sizes,
201
+ alt: image.alt,
202
+ fetchPriority: image.fetchPriority,
203
+ fileSize: image.fileSize,
204
+ isLcpCandidate: image.isLcpCandidate ? "⚠️ YES" : "no",
205
+ }));
206
+ console.table(tableData);
207
+ console.groupEnd();
208
+
209
+ // Elements for inspection
210
+ console.group("🔎 Elements for inspection");
211
+ console.log("Click to expand and inspect in Elements panel:");
212
+ results.nodeArray.forEach((node, i) => {
213
+ const marker = node === lcpCandidate ? " 🚨 LCP" : "";
214
+ console.log(`${i + 1}.${marker}`, node);
215
+ });
216
+ console.groupEnd();
217
+
218
+ // Quick fix suggestion
219
+ console.log("");
220
+ console.group("📝 Suggested Fix");
221
+ console.log("For above-the-fold images, use:");
222
+ console.log("");
223
+ console.log(
224
+ '%c<img src="hero.jpg" fetchpriority="high" alt="...">',
225
+ "font-family: monospace; background: #1e1e1e; color: #9cdcfe; padding: 8px; border-radius: 4px;"
226
+ );
227
+ console.log("");
228
+ console.log("Remove: loading=\"lazy\", data-src, data-lazy");
229
+ console.log("Add: fetchpriority=\"high\" for LCP candidate");
230
+ console.groupEnd();
231
+ }
232
+
233
+ console.groupEnd();
234
+
235
+ return {
236
+ script: "Find-Above-The-Fold-Lazy-Loaded-Images",
237
+ status: "ok",
238
+ count: results.lazyImages.length,
239
+ details: {
240
+ total: results.summary.total,
241
+ withLoadingLazy: results.summary.withLoadingLazy,
242
+ withDataSrc: results.summary.withDataSrc,
243
+ lcpAffected: results.summary.lcpAffected,
244
+ },
245
+ items: results.lazyImages.map((image) => ({
246
+ selector: image.selector,
247
+ lazyType: image.lazyType,
248
+ dimensions: image.dimensions,
249
+ position: image.position,
250
+ distanceFromEdge: image.distanceFromEdge,
251
+ src: image.src,
252
+ srcset: image.srcset,
253
+ sizes: image.sizes,
254
+ alt: image.alt,
255
+ fetchPriority: image.fetchPriority,
256
+ fileSize: image.fileSize,
257
+ isLcpCandidate: image.isLcpCandidate,
258
+ })),
259
+ issues: [
260
+ ...(results.summary.lcpAffected ? [{ severity: "error", message: 'LCP candidate image has lazy loading — remove loading="lazy" and add fetchpriority="high"' }] : []),
261
+ ...(results.lazyImages.filter(i => !i.isLcpCandidate).length > 0 ? [{ severity: "warning", message: `${results.lazyImages.filter(i => !i.isLcpCandidate).length} above-fold image(s) have lazy loading — remove loading="lazy"` }] : []),
262
+ ],
263
+ };
264
+ })();
@@ -0,0 +1,155 @@
1
+ // Find images with contradictory loading="lazy" and fetchpriority="high"
2
+ // https://webperf-snippets.nucliweb.net
3
+
4
+ (() => {
5
+ function getSelector(node) {
6
+ let sel = "";
7
+ try {
8
+ while (node && node.nodeType !== 9) {
9
+ const el = node;
10
+ const name = el.nodeName.toLowerCase();
11
+ const part = el.id
12
+ ? "#" + el.id
13
+ : name +
14
+ (el.classList && el.classList.value && el.classList.value.trim()
15
+ ? "." + el.classList.value.trim().split(/\s+/).slice(0, 2).join(".")
16
+ : "");
17
+ if (sel.length + part.length > 80) return sel || part;
18
+ sel = sel ? part + ">" + sel : part;
19
+ if (el.id) break;
20
+ node = el.parentNode;
21
+ }
22
+ } catch {}
23
+ return sel;
24
+ }
25
+
26
+ function isInViewport(element) {
27
+ const rect = element.getBoundingClientRect();
28
+ return (
29
+ rect.top < window.innerHeight &&
30
+ rect.bottom > 0 &&
31
+ rect.left < window.innerWidth &&
32
+ rect.right > 0 &&
33
+ rect.width > 0 &&
34
+ rect.height > 0
35
+ );
36
+ }
37
+
38
+ // Find LCP candidate for context
39
+ const viewportImages = Array.from(document.querySelectorAll("img")).filter(
40
+ (img) => isInViewport(img) && img.getBoundingClientRect().width > 0
41
+ );
42
+
43
+ let lcpCandidate = null;
44
+ let maxArea = 0;
45
+ viewportImages.forEach((img) => {
46
+ const rect = img.getBoundingClientRect();
47
+ const area = rect.width * rect.height;
48
+ if (area > maxArea) {
49
+ maxArea = area;
50
+ lcpCandidate = img;
51
+ }
52
+ });
53
+
54
+ // Find conflicting elements
55
+ const conflictingElements = document.querySelectorAll(
56
+ "[loading=lazy][fetchpriority=high]"
57
+ );
58
+
59
+ console.group("%c🔍 Lazy + Fetchpriority Conflict Check", "font-weight: bold; font-size: 14px;");
60
+
61
+ if (conflictingElements.length === 0) {
62
+ console.log(
63
+ "%c✅ No conflicts found. No elements have both loading=\"lazy\" and fetchpriority=\"high\".",
64
+ "color: #22c55e; font-weight: bold;"
65
+ );
66
+ } else {
67
+ console.log(
68
+ `%c⚠️ Found ${conflictingElements.length} element(s) with contradictory attributes`,
69
+ "color: #ef4444; font-weight: bold; font-size: 14px;"
70
+ );
71
+ console.log("");
72
+
73
+ // Analyze each element
74
+ const tableData = Array.from(conflictingElements).map((el) => {
75
+ const rect = el.getBoundingClientRect();
76
+ const inViewport = isInViewport(el);
77
+ const isLcp = el === lcpCandidate;
78
+
79
+ return {
80
+ selector: getSelector(el),
81
+ dimensions: `${Math.round(rect.width)}×${Math.round(rect.height)}`,
82
+ inViewport: inViewport ? "Yes" : "No",
83
+ isLcpCandidate: isLcp ? "⚠️ Yes" : "No",
84
+ src: (el.currentSrc || el.src || "").slice(-50),
85
+ element: el,
86
+ };
87
+ });
88
+
89
+ console.log("%c📋 Conflicting Elements:", "font-weight: bold;");
90
+ console.table(tableData.map((item) => ({
91
+ selector: item.selector,
92
+ dimensions: item.dimensions,
93
+ inViewport: item.inViewport,
94
+ isLcpCandidate: item.isLcpCandidate,
95
+ src: item.src,
96
+ })));
97
+
98
+ // Elements for inspection
99
+ console.log("");
100
+ console.log("%c🔎 Elements for inspection:", "font-weight: bold;");
101
+ tableData.forEach(({ element, isLcpCandidate }, i) => {
102
+ const marker = isLcpCandidate === "⚠️ Yes" ? " 🚨 LCP" : "";
103
+ console.log(`${i + 1}.${marker}`, element);
104
+ });
105
+
106
+ // Recommendation
107
+ console.log("");
108
+ console.log("%c📝 How to fix:", "color: #3b82f6; font-weight: bold;");
109
+ console.log("");
110
+
111
+ tableData.forEach(({ selector, inViewport, isLcpCandidate }) => {
112
+ console.log(`%c${selector}:`, "font-weight: bold;");
113
+ if (inViewport === "Yes" || isLcpCandidate === "⚠️ Yes") {
114
+ console.log(" → Image is in viewport. Remove loading=\"lazy\", keep fetchpriority=\"high\"");
115
+ console.log(
116
+ '%c <img src="..." fetchpriority="high">',
117
+ "font-family: monospace; color: #22c55e;"
118
+ );
119
+ } else {
120
+ console.log(" → Image is outside viewport. Remove fetchpriority=\"high\", keep loading=\"lazy\"");
121
+ console.log(
122
+ '%c <img src="..." loading="lazy">',
123
+ "font-family: monospace; color: #22c55e;"
124
+ );
125
+ }
126
+ console.log("");
127
+ });
128
+
129
+ console.log("%c💡 General rule:", "font-weight: bold;");
130
+ console.log(" • Above the fold (LCP): fetchpriority=\"high\", NO lazy loading");
131
+ console.log(" • Below the fold: loading=\"lazy\", NO fetchpriority");
132
+ }
133
+
134
+ console.groupEnd();
135
+
136
+ return {
137
+ script: "Find-Images-With-Lazy-and-Fetchpriority",
138
+ status: "ok",
139
+ count: conflictingElements.length,
140
+ items: Array.from(conflictingElements).map((el) => {
141
+ const rect = el.getBoundingClientRect();
142
+ return {
143
+ selector: getSelector(el),
144
+ src: el.currentSrc || el.src || "",
145
+ inViewport: isInViewport(el),
146
+ isLcpCandidate: el === lcpCandidate,
147
+ widthPx: Math.round(rect.width),
148
+ heightPx: Math.round(rect.height),
149
+ };
150
+ }),
151
+ issues: conflictingElements.length > 0
152
+ ? [{ severity: "error", message: `${conflictingElements.length} element(s) have conflicting loading="lazy" and fetchpriority="high"` }]
153
+ : [],
154
+ };
155
+ })();