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,336 @@
1
+ // Priority Hints Audit
2
+ // https://webperf-snippets.nucliweb.net
3
+
4
+ (() => {
5
+ function isInViewport(el) {
6
+ const rect = el.getBoundingClientRect();
7
+ return (
8
+ rect.top < window.innerHeight &&
9
+ rect.bottom > 0 &&
10
+ rect.left < window.innerWidth &&
11
+ rect.right > 0 &&
12
+ rect.width > 0 &&
13
+ rect.height > 0
14
+ );
15
+ }
16
+
17
+ function findLcpCandidate() {
18
+ const imgs = Array.from(document.querySelectorAll("img")).filter(isInViewport);
19
+ let candidate = null;
20
+ let maxArea = 0;
21
+ imgs.forEach((img) => {
22
+ const { width, height } = img.getBoundingClientRect();
23
+ const area = width * height;
24
+ if (area > maxArea) {
25
+ maxArea = area;
26
+ candidate = img;
27
+ }
28
+ });
29
+ return candidate;
30
+ }
31
+
32
+ function shortUrl(el) {
33
+ const src = el.src || el.href || el.getAttribute("href") || "";
34
+ return src.split("/").pop()?.split("?")[0] || src.slice(-50) || "(unknown)";
35
+ }
36
+
37
+ // Build performance entry map for timing lookups
38
+ const entryMap = new Map();
39
+ performance.getEntriesByType("resource").forEach((e) => {
40
+ try {
41
+ entryMap.set(new URL(e.name, location.origin).href, e);
42
+ } catch {}
43
+ });
44
+
45
+ function getEntry(el) {
46
+ const src = el.src || el.href || el.getAttribute("href") || "";
47
+ if (!src) return null;
48
+ try {
49
+ return entryMap.get(new URL(src, location.origin).href) || null;
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ // All elements with fetchpriority
56
+ const allElements = Array.from(document.querySelectorAll("[fetchpriority]"));
57
+ // Exclude images — covered by "Find Images With Lazy and Fetchpriority"
58
+ const nonImageElements = allElements.filter((el) => el.tagName !== "IMG");
59
+ const images = allElements.filter((el) => el.tagName === "IMG");
60
+ const preloadsWithPriority = Array.from(
61
+ document.querySelectorAll('link[rel="preload"][fetchpriority]')
62
+ );
63
+ const lcpCandidate = findLcpCandidate();
64
+
65
+ const issues = [];
66
+
67
+ // Issue 1: <link rel="preload"> with fetchpriority="low" (contradictory)
68
+ preloadsWithPriority
69
+ .filter((link) => link.getAttribute("fetchpriority") === "low")
70
+ .forEach((link) => {
71
+ issues.push({
72
+ severity: "error",
73
+ element: link,
74
+ resource: shortUrl(link),
75
+ message: 'preload + fetchpriority="low" is contradictory',
76
+ detail:
77
+ 'preload signals this resource is critical for the current page; fetchpriority="low" contradicts that.',
78
+ fix: 'Remove fetchpriority="low", or use fetchpriority="high" if the resource is truly critical',
79
+ });
80
+ });
81
+
82
+ // Issue 2: fetchpriority="low" on resources loaded in the first 500ms
83
+ allElements
84
+ .filter((el) => el.getAttribute("fetchpriority") === "low")
85
+ .forEach((el) => {
86
+ const entry = getEntry(el);
87
+ if (entry && entry.startTime < 500) {
88
+ issues.push({
89
+ severity: "warning",
90
+ element: el,
91
+ resource: shortUrl(el),
92
+ message: `fetchpriority="low" but loaded at ${entry.startTime.toFixed(0)}ms`,
93
+ detail:
94
+ "This resource loaded very early. If it's genuinely non-critical, the browser is deprioritizing it correctly. Otherwise, reconsider the priority.",
95
+ fix: 'Verify the resource is non-critical, or remove fetchpriority="low"',
96
+ });
97
+ }
98
+ });
99
+
100
+ // Issue 3: fetchpriority="high" on resources loaded after 3000ms
101
+ allElements
102
+ .filter((el) => el.getAttribute("fetchpriority") === "high")
103
+ .forEach((el) => {
104
+ const entry = getEntry(el);
105
+ if (entry && entry.startTime > 3000) {
106
+ issues.push({
107
+ severity: "warning",
108
+ element: el,
109
+ resource: shortUrl(el),
110
+ message: `fetchpriority="high" but loaded at ${entry.startTime.toFixed(0)}ms`,
111
+ detail:
112
+ "High-priority resources should load early. A late start may indicate render-blocking dependencies or incorrect placement in the document.",
113
+ fix: "Move the element earlier in the HTML or investigate render-blocking dependencies",
114
+ });
115
+ }
116
+ });
117
+
118
+ // Issue 4: LCP candidate image without fetchpriority="high"
119
+ if (lcpCandidate && lcpCandidate.getAttribute("fetchpriority") !== "high") {
120
+ const fp = lcpCandidate.getAttribute("fetchpriority");
121
+ issues.push({
122
+ severity: "warning",
123
+ element: lcpCandidate,
124
+ resource:
125
+ (lcpCandidate.currentSrc || lcpCandidate.src).split("/").pop()?.split("?")[0] ||
126
+ "LCP image",
127
+ message: `LCP candidate image ${fp ? `has fetchpriority="${fp}"` : "has no fetchpriority set"}`,
128
+ detail:
129
+ 'The largest visible image is likely the LCP element and benefits most from fetchpriority="high".',
130
+ fix: 'Add fetchpriority="high" to the LCP image to improve Largest Contentful Paint',
131
+ });
132
+ }
133
+
134
+ // Display results
135
+ console.group("%cšŸŽÆ Priority Hints Audit", "font-weight: bold; font-size: 14px;");
136
+
137
+ const highCount = allElements.filter(
138
+ (el) => el.getAttribute("fetchpriority") === "high"
139
+ ).length;
140
+ const lowCount = allElements.filter(
141
+ (el) => el.getAttribute("fetchpriority") === "low"
142
+ ).length;
143
+ const autoCount = allElements.filter(
144
+ (el) => el.getAttribute("fetchpriority") === "auto"
145
+ ).length;
146
+
147
+ console.log("");
148
+ console.log("%cInventory:", "font-weight: bold;");
149
+ console.log(` Total elements with fetchpriority: ${allElements.length}`);
150
+ console.log(
151
+ ` • Images: ${images.length} (see "Find Images With Lazy and Fetchpriority" for image-specific checks)`
152
+ );
153
+ console.log(` • Non-image resources: ${nonImageElements.length}`);
154
+ console.log(` • Preloads with fetchpriority: ${preloadsWithPriority.length}`);
155
+ console.log(
156
+ ` Priority breakdown: high=${highCount}, low=${lowCount}, auto=${autoCount}`
157
+ );
158
+
159
+ if (allElements.length === 0 && !lcpCandidate) {
160
+ console.log("");
161
+ console.log(
162
+ "%cā„¹ļø No fetchpriority attributes found on this page.",
163
+ "color: #3b82f6;"
164
+ );
165
+ console.groupEnd();
166
+ return { script: "Priority-Hints-Audit", status: "ok", count: 0, items: [], issues: [] };
167
+ }
168
+
169
+ // LCP candidate status
170
+ if (lcpCandidate) {
171
+ const fp = lcpCandidate.getAttribute("fetchpriority");
172
+ const status =
173
+ fp === "high"
174
+ ? 'āœ… fetchpriority="high"'
175
+ : fp
176
+ ? `āš ļø fetchpriority="${fp}"`
177
+ : "āš ļø No fetchpriority set";
178
+ console.log("");
179
+ console.log("%cLCP Candidate:", "font-weight: bold;");
180
+ console.log(` ${status}`);
181
+ console.log(" Element:", lcpCandidate);
182
+ }
183
+
184
+ // Non-image resources table
185
+ if (nonImageElements.length > 0) {
186
+ console.log("");
187
+ console.group(
188
+ `%cšŸ“‹ Non-Image Resources with fetchpriority (${nonImageElements.length})`,
189
+ "font-weight: bold;"
190
+ );
191
+
192
+ console.table(
193
+ nonImageElements.map((el) => {
194
+ const entry = getEntry(el);
195
+ const rel = el.getAttribute("rel") || "";
196
+ const as = el.getAttribute("as") || "";
197
+ const tag =
198
+ el.tagName.toLowerCase() +
199
+ (rel ? `[rel="${rel}"]` : "") +
200
+ (as ? `[as="${as}"]` : "");
201
+ return {
202
+ Tag: tag,
203
+ Priority: el.getAttribute("fetchpriority"),
204
+ "Start (ms)": entry ? entry.startTime.toFixed(0) : "N/A",
205
+ "Size (KB)":
206
+ entry?.decodedBodySize
207
+ ? (entry.decodedBodySize / 1024).toFixed(1)
208
+ : "N/A",
209
+ Resource: shortUrl(el),
210
+ };
211
+ })
212
+ );
213
+
214
+ console.log("%cšŸ”Ž Elements:", "font-weight: bold;");
215
+ nonImageElements.forEach((el, i) => console.log(`${i + 1}.`, el));
216
+ console.groupEnd();
217
+ }
218
+
219
+ // Preload links with fetchpriority
220
+ if (preloadsWithPriority.length > 0) {
221
+ console.log("");
222
+ console.group(
223
+ `%cšŸ”— Preload Links with fetchpriority (${preloadsWithPriority.length})`,
224
+ "font-weight: bold;"
225
+ );
226
+
227
+ console.table(
228
+ preloadsWithPriority.map((link) => {
229
+ const fp = link.getAttribute("fetchpriority");
230
+ const entry = getEntry(link);
231
+ return {
232
+ fetchpriority: fp,
233
+ as: link.getAttribute("as") || "(missing)",
234
+ Status:
235
+ fp === "low" ? "āš ļø Conflict" : fp === "high" ? "āœ… Good" : "ā„¹ļø Auto",
236
+ "Start (ms)": entry ? entry.startTime.toFixed(0) : "N/A",
237
+ Resource: shortUrl(link),
238
+ };
239
+ })
240
+ );
241
+
242
+ console.groupEnd();
243
+ }
244
+
245
+ // Issues
246
+ const errors = issues.filter((i) => i.severity === "error");
247
+ const warnings = issues.filter((i) => i.severity === "warning");
248
+
249
+ if (issues.length > 0) {
250
+ console.log("");
251
+ console.group(
252
+ `%cāš ļø Issues Found (${errors.length} errors, ${warnings.length} warnings)`,
253
+ "color: #ef4444; font-weight: bold;"
254
+ );
255
+
256
+ [...errors, ...warnings].forEach((issue) => {
257
+ const color = issue.severity === "error" ? "#ef4444" : "#f59e0b";
258
+ const icon = issue.severity === "error" ? "šŸ”“" : "āš ļø";
259
+ console.log("");
260
+ console.log(
261
+ `%c${icon} ${issue.message}`,
262
+ `font-weight: bold; color: ${color};`
263
+ );
264
+ console.log(` ${issue.detail}`);
265
+ console.log(` Fix: ${issue.fix}`);
266
+ console.log(" Element:", issue.element);
267
+ });
268
+
269
+ console.groupEnd();
270
+ } else {
271
+ console.log("");
272
+ console.log(
273
+ "%cāœ… No priority hints issues detected.",
274
+ "color: #22c55e; font-weight: bold;"
275
+ );
276
+ }
277
+
278
+ // Quick reference
279
+ console.log("");
280
+ console.group(
281
+ "%cšŸ“ fetchpriority Quick Reference",
282
+ "color: #3b82f6; font-weight: bold;"
283
+ );
284
+ console.log("");
285
+ console.log("%c āœ… LCP hero image:", "color: #22c55e;");
286
+ console.log(
287
+ '%c <img src="hero.webp" fetchpriority="high" alt="...">',
288
+ "font-family: monospace;"
289
+ );
290
+ console.log("");
291
+ console.log("%c āœ… Preloaded critical font:", "color: #22c55e;");
292
+ console.log(
293
+ '%c <link rel="preload" href="font.woff2" as="font" fetchpriority="high" crossorigin>',
294
+ "font-family: monospace;"
295
+ );
296
+ console.log("");
297
+ console.log("%c āœ… Below-fold image:", "color: #22c55e;");
298
+ console.log(
299
+ '%c <img src="content.jpg" loading="lazy" fetchpriority="low" alt="...">',
300
+ "font-family: monospace;"
301
+ );
302
+ console.log("");
303
+ console.log("%c āŒ Contradictory — preload + low priority:", "color: #ef4444;");
304
+ console.log(
305
+ '%c <link rel="preload" href="font.woff2" as="font" fetchpriority="low">',
306
+ "font-family: monospace;"
307
+ );
308
+ console.groupEnd();
309
+
310
+ console.log("");
311
+ console.log(
312
+ '%cšŸ’” Related: "Find Images With Lazy and Fetchpriority" detects the lazy+high conflict on images. https://webperf-snippets.nucliweb.net/Loading/Find-Images-With-Lazy-and-Fetchpriority',
313
+ "color: #6b7280;"
314
+ );
315
+
316
+ console.groupEnd();
317
+
318
+ return {
319
+ script: "Priority-Hints-Audit",
320
+ status: "ok",
321
+ count: allElements.length,
322
+ details: {
323
+ highCount,
324
+ lowCount,
325
+ autoCount,
326
+ preloadsWithPriorityCount: preloadsWithPriority.length,
327
+ lcpCandidateHasFetchpriority: lcpCandidate ? lcpCandidate.getAttribute("fetchpriority") === "high" : null,
328
+ },
329
+ items: allElements.map(el => ({
330
+ tag: el.tagName.toLowerCase(),
331
+ fetchpriority: el.getAttribute("fetchpriority"),
332
+ resource: (el.src || el.href || el.getAttribute("href") || "").split("/").pop()?.split("?")[0] || "",
333
+ })),
334
+ issues: issues.map(i => ({ severity: i.severity, message: i.message })),
335
+ };
336
+ })();