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,300 @@
1
+ // Execute after page load without user interaction (scroll, click, etc)
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 isInHiddenContainer(element) {
39
+ let parent = element.parentElement;
40
+ const hiddenSelectors = [
41
+ "[hidden]",
42
+ '[aria-hidden="true"]',
43
+ ".modal:not(.show)",
44
+ ".tab-pane:not(.active)",
45
+ '[role="tabpanel"]:not(.active)',
46
+ ".accordion-collapse:not(.show)",
47
+ ".carousel-item:not(.active)",
48
+ ".swiper-slide:not(.swiper-slide-active)",
49
+ ];
50
+
51
+ while (parent && parent !== document.body) {
52
+ const cs = window.getComputedStyle(parent);
53
+ if (cs.display === "none" || cs.visibility === "hidden") {
54
+ return { hidden: true, reason: "CSS hidden", container: getSelector(parent) };
55
+ }
56
+ for (const selector of hiddenSelectors) {
57
+ try {
58
+ if (parent.matches(selector)) {
59
+ return { hidden: true, reason: selector, container: getSelector(parent) };
60
+ }
61
+ } catch {}
62
+ }
63
+ parent = parent.parentElement;
64
+ }
65
+ return { hidden: false };
66
+ }
67
+
68
+ function getImageSize(imgElement) {
69
+ const src = imgElement.currentSrc || imgElement.src;
70
+ if (!src || src.startsWith("data:")) return 0;
71
+
72
+ const perfEntries = performance.getEntriesByType("resource");
73
+ const imgEntry = perfEntries.find((entry) => entry.name === src);
74
+
75
+ if (imgEntry) {
76
+ return imgEntry.transferSize || imgEntry.encodedBodySize || 0;
77
+ }
78
+ return 0;
79
+ }
80
+
81
+ function formatBytes(bytes) {
82
+ if (bytes === 0) return "0 Bytes";
83
+ const k = 1024;
84
+ const sizes = ["Bytes", "KB", "MB", "GB"];
85
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
86
+ return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
87
+ }
88
+
89
+ // Find LCP candidate (largest visible image) to exclude from recommendations
90
+ const allViewportImages = Array.from(document.querySelectorAll("img")).filter(
91
+ (img) => isInViewport(img) && img.getBoundingClientRect().width > 0
92
+ );
93
+
94
+ let lcpCandidate = null;
95
+ let maxArea = 0;
96
+
97
+ allViewportImages.forEach((img) => {
98
+ const rect = img.getBoundingClientRect();
99
+ const area = rect.width * rect.height;
100
+ if (area > maxArea) {
101
+ maxArea = area;
102
+ lcpCandidate = img;
103
+ }
104
+ });
105
+
106
+ // Find images without lazy loading
107
+ const notLazyImages = document.querySelectorAll('img:not([data-src]):not([loading="lazy"])');
108
+
109
+ const results = {
110
+ belowFold: [],
111
+ hiddenContainers: [],
112
+ excluded: {
113
+ inViewport: 0,
114
+ lcpCandidate: null,
115
+ tooSmall: 0,
116
+ },
117
+ elements: [],
118
+ };
119
+
120
+ notLazyImages.forEach((img) => {
121
+ const rect = img.getBoundingClientRect();
122
+
123
+ // Skip images in viewport (they shouldn't be lazy loaded)
124
+ if (isInViewport(img)) {
125
+ if (img === lcpCandidate) {
126
+ results.excluded.lcpCandidate = getSelector(img);
127
+ } else {
128
+ results.excluded.inViewport++;
129
+ }
130
+ return;
131
+ }
132
+
133
+ // Skip very small images (likely icons/tracking pixels)
134
+ if (rect.width < 50 || rect.height < 50) {
135
+ results.excluded.tooSmall++;
136
+ return;
137
+ }
138
+
139
+ const src = img.currentSrc || img.src;
140
+ const size = getImageSize(img);
141
+ const hiddenCheck = isInHiddenContainer(img);
142
+
143
+ const imageData = {
144
+ selector: getSelector(img),
145
+ src: src.length > 60 ? "..." + src.slice(-57) : src,
146
+ fullSrc: src,
147
+ dimensions: `${img.naturalWidth}×${img.naturalHeight}`,
148
+ size: size,
149
+ sizeFormatted: size > 0 ? formatBytes(size) : "unknown",
150
+ element: img,
151
+ };
152
+
153
+ if (hiddenCheck.hidden) {
154
+ imageData.hiddenReason = hiddenCheck.reason;
155
+ imageData.container = hiddenCheck.container;
156
+ results.hiddenContainers.push(imageData);
157
+ } else {
158
+ imageData.distanceFromViewport = Math.round(rect.top - window.innerHeight) + "px";
159
+ results.belowFold.push(imageData);
160
+ }
161
+
162
+ results.elements.push(img);
163
+ });
164
+
165
+ // Sort below-fold images by distance (furthest first)
166
+ results.belowFold.sort((a, b) => parseInt(b.distanceFromViewport) - parseInt(a.distanceFromViewport));
167
+
168
+ const totalImages = results.belowFold.length + results.hiddenContainers.length;
169
+ const totalSize = [...results.belowFold, ...results.hiddenContainers].reduce((sum, img) => sum + img.size, 0);
170
+
171
+ // Display results
172
+ console.group("💡 Lazy Loading Opportunities");
173
+
174
+ if (totalImages === 0) {
175
+ console.log(
176
+ "%c✅ Good job! All images outside the viewport have lazy loading.",
177
+ "background: #222; color: #22c55e; padding: 0.5ch 1ch; font-weight: bold"
178
+ );
179
+ } else {
180
+ console.log(
181
+ `%c⚠️ Found ${totalImages} image(s) that should have lazy loading`,
182
+ "color: #f59e0b; font-weight: bold; font-size: 14px"
183
+ );
184
+
185
+ if (totalSize > 0) {
186
+ console.log(
187
+ `%c📊 Potential savings: ${formatBytes(totalSize)} on initial load`,
188
+ "color: #22c55e; font-weight: bold"
189
+ );
190
+ }
191
+ console.log("");
192
+
193
+ // Below the fold images
194
+ if (results.belowFold.length > 0) {
195
+ console.group(`📍 Below The Fold (${results.belowFold.length} images)`);
196
+ const tableData = results.belowFold.slice(0, 20).map((img) => ({
197
+ selector: img.selector,
198
+ src: img.src,
199
+ dimensions: img.dimensions,
200
+ size: img.size,
201
+ sizeFormatted: img.sizeFormatted,
202
+ distanceFromViewport: img.distanceFromViewport,
203
+ }));
204
+ console.table(tableData);
205
+ if (results.belowFold.length > 20) {
206
+ console.log(`... and ${results.belowFold.length - 20} more images`);
207
+ }
208
+ console.groupEnd();
209
+ }
210
+
211
+ // Hidden container images
212
+ if (results.hiddenContainers.length > 0) {
213
+ console.log("");
214
+ console.group(`🔒 In Hidden Containers (${results.hiddenContainers.length} images)`);
215
+ console.log("Images in tabs, modals, carousels, or other hidden elements:");
216
+ console.log("");
217
+ const tableData = results.hiddenContainers.slice(0, 15).map((img) => ({
218
+ selector: img.selector,
219
+ src: img.src,
220
+ dimensions: img.dimensions,
221
+ size: img.size,
222
+ sizeFormatted: img.sizeFormatted,
223
+ hiddenReason: img.hiddenReason,
224
+ container: img.container,
225
+ }));
226
+ console.table(tableData);
227
+ if (results.hiddenContainers.length > 15) {
228
+ console.log(`... and ${results.hiddenContainers.length - 15} more images`);
229
+ }
230
+ console.groupEnd();
231
+ }
232
+
233
+ // Excluded summary
234
+ console.log("");
235
+ console.group("ℹ️ Correctly Excluded (should NOT be lazy loaded)");
236
+ console.log(`• LCP candidate: ${results.excluded.lcpCandidate || "none detected"}`);
237
+ console.log(`• Other in-viewport images: ${results.excluded.inViewport}`);
238
+ console.log(`• Too small (<50px): ${results.excluded.tooSmall}`);
239
+ console.groupEnd();
240
+
241
+ // Elements for inspection
242
+ console.log("");
243
+ console.group("🔎 Elements for inspection");
244
+ console.log("Click to inspect in Elements panel:");
245
+ results.elements.slice(0, 15).forEach((img, i) => console.log(`${i + 1}.`, img));
246
+ if (results.elements.length > 15) {
247
+ console.log(`... and ${results.elements.length - 15} more`);
248
+ }
249
+ console.groupEnd();
250
+
251
+ // Quick fix
252
+ console.log("");
253
+ console.group("📝 Quick Fix");
254
+ console.log("Add lazy loading to these images:");
255
+ console.log("");
256
+ console.log(
257
+ '%c<img src="image.jpg" loading="lazy" alt="...">',
258
+ "font-family: monospace; background: #1e1e1e; color: #9cdcfe; padding: 8px; border-radius: 4px"
259
+ );
260
+ console.groupEnd();
261
+ }
262
+
263
+ console.groupEnd();
264
+
265
+ return {
266
+ script: "Find-non-Lazy-Loaded-Images-outside-of-the-viewport",
267
+ status: "ok",
268
+ count: totalImages,
269
+ details: {
270
+ belowFoldCount: results.belowFold.length,
271
+ hiddenContainerCount: results.hiddenContainers.length,
272
+ totalSizeBytes: totalSize,
273
+ excluded: {
274
+ inViewport: results.excluded.inViewport,
275
+ lcpCandidate: results.excluded.lcpCandidate,
276
+ tooSmall: results.excluded.tooSmall,
277
+ },
278
+ },
279
+ items: [
280
+ ...results.belowFold.map((img) => ({
281
+ selector: img.selector,
282
+ src: img.fullSrc,
283
+ sizeBytes: img.size,
284
+ category: "below-fold",
285
+ distancePx: parseInt(img.distanceFromViewport),
286
+ })),
287
+ ...results.hiddenContainers.map((img) => ({
288
+ selector: img.selector,
289
+ src: img.fullSrc,
290
+ sizeBytes: img.size,
291
+ category: "hidden-container",
292
+ hiddenReason: img.hiddenReason,
293
+ container: img.container,
294
+ })),
295
+ ],
296
+ issues: totalImages > 0
297
+ ? [{ severity: "warning", message: `${totalImages} image(s) outside the viewport are missing loading="lazy"` }]
298
+ : [],
299
+ };
300
+ })();
@@ -0,0 +1,161 @@
1
+ // Find render-blocking resources
2
+ // https://webperf-snippets.nucliweb.net
3
+
4
+ (() => {
5
+ // Check browser support
6
+ const testEntry = performance.getEntriesByType("resource")[0];
7
+ if (testEntry && !("renderBlockingStatus" in testEntry)) {
8
+ console.log(
9
+ "%c⚠️ renderBlockingStatus not supported in this browser",
10
+ "color: #f59e0b; font-weight: bold;"
11
+ );
12
+ console.log("This API is currently Chromium-only (Chrome 107+, Edge 107+).");
13
+ console.log("Try using Chrome or Edge to analyze render-blocking resources.");
14
+ return { script: "Find-render-blocking-resources", status: "unsupported", error: "renderBlockingStatus API requires Chrome 107+" };
15
+ }
16
+
17
+ const blockingResources = performance
18
+ .getEntriesByType("resource")
19
+ .filter((entry) => entry.renderBlockingStatus === "blocking")
20
+ .map((entry) => {
21
+ const url = new URL(entry.name);
22
+ return {
23
+ name: entry.name,
24
+ shortName: url.pathname.split("/").pop() || url.pathname,
25
+ type: entry.initiatorType,
26
+ startTime: entry.startTime,
27
+ duration: entry.duration,
28
+ responseEnd: entry.responseEnd,
29
+ size: entry.transferSize || 0,
30
+ };
31
+ })
32
+ .sort((a, b) => b.responseEnd - a.responseEnd);
33
+
34
+ console.group("%c🚧 Render-Blocking Resources", "font-weight: bold; font-size: 14px;");
35
+
36
+ if (blockingResources.length === 0) {
37
+ console.log(
38
+ "%c✅ No render-blocking resources found!",
39
+ "color: #22c55e; font-weight: bold;"
40
+ );
41
+ console.log("The page has no resources blocking initial render.");
42
+ console.log("");
43
+ console.log("%c💡 This could mean:", "font-weight: bold;");
44
+ console.log(" • CSS is inlined or loaded asynchronously");
45
+ console.log(" • Scripts use async or defer attributes");
46
+ console.log(" • Critical resources are optimized");
47
+ } else {
48
+ // Calculate metrics
49
+ const lastBlockingEnd = Math.max(...blockingResources.map((r) => r.responseEnd));
50
+ const totalSize = blockingResources.reduce((sum, r) => sum + r.size, 0);
51
+ const byType = blockingResources.reduce((acc, r) => {
52
+ acc[r.type] = (acc[r.type] || 0) + 1;
53
+ return acc;
54
+ }, {});
55
+
56
+ // Summary
57
+ console.log(
58
+ `%c⚠️ Found ${blockingResources.length} render-blocking resource(s)`,
59
+ "color: #ef4444; font-weight: bold; font-size: 14px;"
60
+ );
61
+ console.log("");
62
+
63
+ console.log("%c📊 Impact Summary:", "font-weight: bold;");
64
+ console.log(` Rendering blocked until: ${lastBlockingEnd.toFixed(0)}ms`);
65
+ console.log(` Total blocking resources: ${blockingResources.length}`);
66
+ if (totalSize > 0) {
67
+ const sizeKB = (totalSize / 1024).toFixed(1);
68
+ console.log(` Total size: ${sizeKB} KB`);
69
+ }
70
+ console.log(` By type: ${Object.entries(byType).map(([k, v]) => `${k} (${v})`).join(", ")}`);
71
+
72
+ // Timeline visualization
73
+ console.log("");
74
+ console.log("%c⏱️ Timeline (sorted by completion):", "font-weight: bold;");
75
+
76
+ const formatBar = (end, max) => {
77
+ const pct = (end / max) * 100;
78
+ const width = Math.round(pct / 5);
79
+ return "█".repeat(width) + "░".repeat(20 - width);
80
+ };
81
+
82
+ blockingResources.forEach((r) => {
83
+ const bar = formatBar(r.responseEnd, lastBlockingEnd);
84
+ const sizeInfo = r.size > 0 ? ` (${(r.size / 1024).toFixed(1)} KB)` : "";
85
+ console.log(
86
+ ` ${r.type.padEnd(8)} ${r.responseEnd.toFixed(0).padStart(5)}ms ${bar} ${r.shortName}${sizeInfo}`
87
+ );
88
+ });
89
+
90
+ // Table with details
91
+ console.log("");
92
+ console.log("%c📋 Details:", "font-weight: bold;");
93
+ const tableData = blockingResources.map((r) => ({
94
+ Type: r.type,
95
+ "Response End": r.responseEnd.toFixed(0) + "ms",
96
+ Duration: r.duration.toFixed(0) + "ms",
97
+ Size: r.size > 0 ? (r.size / 1024).toFixed(1) + " KB" : "N/A",
98
+ Resource: r.name.length > 60 ? "..." + r.name.slice(-57) : r.name,
99
+ }));
100
+ console.table(tableData);
101
+
102
+ // Recommendations
103
+ console.log("");
104
+ console.log("%c📝 How to fix:", "color: #3b82f6; font-weight: bold;");
105
+ console.log("");
106
+
107
+ if (byType.script) {
108
+ console.log("%c Scripts:", "font-weight: bold;");
109
+ console.log(" • Add 'defer' for scripts that don't need to run immediately");
110
+ console.log(" • Add 'async' for independent scripts (analytics, etc.)");
111
+ console.log(" • Move non-critical scripts to end of <body>");
112
+ console.log('%c <script src="app.js" defer></script>', "font-family: monospace; color: #22c55e;");
113
+ console.log("");
114
+ }
115
+
116
+ if (byType.link || byType.css) {
117
+ console.log("%c CSS:", "font-weight: bold;");
118
+ console.log(" • Inline critical CSS in <head>");
119
+ console.log(" • Load non-critical CSS asynchronously with media='print' trick");
120
+ console.log(" • Use <link rel='preload'> for critical stylesheets");
121
+ console.log('%c <link rel="preload" href="style.css" as="style" onload="this.rel=\'stylesheet\'">', "font-family: monospace; color: #22c55e;");
122
+ console.log("");
123
+ }
124
+
125
+ if (byType.font) {
126
+ console.log("%c Fonts:", "font-weight: bold;");
127
+ console.log(" • Use font-display: swap in @font-face");
128
+ console.log(" • Preload critical fonts");
129
+ console.log(" • Consider system font stack for initial render");
130
+ console.log('%c <link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>', "font-family: monospace; color: #22c55e;");
131
+ console.log("");
132
+ }
133
+ }
134
+
135
+ console.groupEnd();
136
+
137
+ const lastBlockingEnd = blockingResources.length ? Math.max(...blockingResources.map((r) => r.responseEnd)) : 0;
138
+ const totalSizeBytes = blockingResources.reduce((sum, r) => sum + r.size, 0);
139
+ const byType = blockingResources.reduce((acc, r) => { acc[r.type] = (acc[r.type] || 0) + 1; return acc; }, {});
140
+ return {
141
+ script: "Find-render-blocking-resources",
142
+ status: "ok",
143
+ count: blockingResources.length,
144
+ details: {
145
+ totalBlockingUntilMs: Math.round(lastBlockingEnd),
146
+ totalSizeBytes,
147
+ byType,
148
+ },
149
+ items: blockingResources.map((r) => ({
150
+ type: r.type,
151
+ url: r.name,
152
+ shortName: r.shortName,
153
+ responseEndMs: Math.round(r.responseEnd),
154
+ durationMs: Math.round(r.duration),
155
+ sizeBytes: r.size,
156
+ })),
157
+ issues: blockingResources.length > 0
158
+ ? [{ severity: "error", message: `${blockingResources.length} render-blocking resource(s) delay rendering until ${Math.round(lastBlockingEnd)}ms` }]
159
+ : [],
160
+ };
161
+ })();