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,400 @@
1
+ // Resource Hints Analysis
2
+ // https://webperf-snippets.nucliweb.net
3
+
4
+ (() => {
5
+ const formatBytes = (bytes) => {
6
+ if (!bytes || bytes === 0) return "-";
7
+ const k = 1024;
8
+ const sizes = ["B", "KB", "MB"];
9
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
10
+ return (bytes / Math.pow(k, i)).toFixed(1) + " " + sizes[i];
11
+ };
12
+
13
+ // Resource hint definitions
14
+ const hintTypes = [
15
+ {
16
+ rel: "preload",
17
+ icon: "⚔",
18
+ description: "Critical resource for current page",
19
+ requiresAs: true,
20
+ },
21
+ {
22
+ rel: "modulepreload",
23
+ icon: "šŸ“¦",
24
+ description: "ES module for current page",
25
+ requiresAs: false,
26
+ },
27
+ {
28
+ rel: "preconnect",
29
+ icon: "šŸ”Œ",
30
+ description: "Early connection to origin",
31
+ requiresAs: false,
32
+ },
33
+ {
34
+ rel: "dns-prefetch",
35
+ icon: "šŸ”",
36
+ description: "DNS lookup for origin",
37
+ requiresAs: false,
38
+ },
39
+ {
40
+ rel: "prefetch",
41
+ icon: "šŸ“„",
42
+ description: "Resource for future navigation",
43
+ requiresAs: false,
44
+ },
45
+ {
46
+ rel: "prerender",
47
+ icon: "šŸ–¼ļø",
48
+ description: "Pre-render page",
49
+ requiresAs: false,
50
+ },
51
+ ];
52
+
53
+ // Get all link elements with resource hints
54
+ const allHints = [];
55
+ const issues = [];
56
+
57
+ hintTypes.forEach((type) => {
58
+ const links = document.querySelectorAll(`link[rel="${type.rel}"]`);
59
+ links.forEach((link) => {
60
+ const href = link.href || link.getAttribute("href") || "";
61
+ let hostname = "";
62
+ try {
63
+ hostname = new URL(href, location.origin).hostname;
64
+ } catch {}
65
+
66
+ const hint = {
67
+ rel: type.rel,
68
+ icon: type.icon,
69
+ href,
70
+ shortHref: href.length > 50 ? "..." + href.slice(-47) : href,
71
+ hostname,
72
+ as: link.getAttribute("as") || "",
73
+ crossorigin: link.crossOrigin || link.getAttribute("crossorigin") || "",
74
+ fetchpriority: link.fetchPriority || link.getAttribute("fetchpriority") || "",
75
+ type: link.type || "",
76
+ media: link.media || "",
77
+ element: link,
78
+ };
79
+
80
+ allHints.push(hint);
81
+
82
+ // Check for issues
83
+ if (type.requiresAs && !hint.as) {
84
+ issues.push({
85
+ hint,
86
+ issue: "Missing 'as' attribute (required for preload)",
87
+ severity: "error",
88
+ fix: `Add as="script|style|font|image|fetch"`,
89
+ });
90
+ }
91
+
92
+ if (hint.as === "font" && !hint.crossorigin) {
93
+ issues.push({
94
+ hint,
95
+ issue: "Font preload without crossorigin (causes double fetch)",
96
+ severity: "error",
97
+ fix: 'Add crossorigin="anonymous"',
98
+ });
99
+ }
100
+
101
+ if (type.rel === "preconnect" && !hint.crossorigin) {
102
+ // Check if we load CORS resources from this origin
103
+ const corsResources = performance
104
+ .getEntriesByType("resource")
105
+ .filter(
106
+ (r) =>
107
+ r.name.includes(hostname) &&
108
+ (r.initiatorType === "script" ||
109
+ r.initiatorType === "css" ||
110
+ r.initiatorType === "font")
111
+ );
112
+ if (corsResources.length > 0) {
113
+ issues.push({
114
+ hint,
115
+ issue: "Preconnect may need crossorigin for CORS resources",
116
+ severity: "warning",
117
+ fix: 'Consider adding crossorigin="anonymous"',
118
+ });
119
+ }
120
+ }
121
+ });
122
+ });
123
+
124
+ // Check for unused preloads
125
+ const preloads = allHints.filter((h) => h.rel === "preload");
126
+ const loadedResources = performance.getEntriesByType("resource");
127
+
128
+ preloads.forEach((preload) => {
129
+ const wasUsed = loadedResources.some((r) => r.name === preload.href);
130
+ if (!wasUsed) {
131
+ issues.push({
132
+ hint: preload,
133
+ issue: "Preloaded resource not used within page load",
134
+ severity: "warning",
135
+ fix: "Remove unused preload or verify it's needed",
136
+ });
137
+ }
138
+ });
139
+
140
+ // Check for preconnects without loaded resources
141
+ const preconnects = allHints.filter(
142
+ (h) => h.rel === "preconnect" || h.rel === "dns-prefetch"
143
+ );
144
+ preconnects.forEach((pc) => {
145
+ const hasResources = loadedResources.some((r) => {
146
+ try {
147
+ return new URL(r.name).hostname === pc.hostname;
148
+ } catch {
149
+ return false;
150
+ }
151
+ });
152
+ if (!hasResources && pc.hostname) {
153
+ issues.push({
154
+ hint: pc,
155
+ issue: `No resources loaded from ${pc.hostname}`,
156
+ severity: "warning",
157
+ fix: "Remove unused preconnect or verify it's needed for future resources",
158
+ });
159
+ }
160
+ });
161
+
162
+ // Check for too many preloads
163
+ if (preloads.length > 6) {
164
+ issues.push({
165
+ hint: { rel: "preload", href: `${preloads.length} preloads` },
166
+ issue: `Too many preloads (${preloads.length}) - they compete for bandwidth`,
167
+ severity: "warning",
168
+ fix: "Limit to 5-6 most critical resources",
169
+ });
170
+ }
171
+
172
+ // Get third-party origins that could benefit from preconnect
173
+ const thirdPartyOrigins = new Map();
174
+ loadedResources.forEach((r) => {
175
+ try {
176
+ const url = new URL(r.name);
177
+ if (url.hostname !== location.hostname) {
178
+ if (!thirdPartyOrigins.has(url.origin)) {
179
+ thirdPartyOrigins.set(url.origin, { count: 0, size: 0, types: new Set() });
180
+ }
181
+ const data = thirdPartyOrigins.get(url.origin);
182
+ data.count++;
183
+ data.size += r.transferSize || 0;
184
+ data.types.add(r.initiatorType);
185
+ }
186
+ } catch {}
187
+ });
188
+
189
+ // Find origins without preconnect
190
+ const preconnectedOrigins = new Set(
191
+ [...preconnects].map((p) => {
192
+ try {
193
+ return new URL(p.href).origin;
194
+ } catch {
195
+ return "";
196
+ }
197
+ })
198
+ );
199
+
200
+ const missingPreconnects = [];
201
+ thirdPartyOrigins.forEach((data, origin) => {
202
+ if (!preconnectedOrigins.has(origin) && data.count >= 2) {
203
+ missingPreconnects.push({ origin, ...data });
204
+ }
205
+ });
206
+
207
+ // Display results
208
+ console.group("%cšŸ”— Resource Hints Analysis", "font-weight: bold; font-size: 14px;");
209
+
210
+ // Summary
211
+ console.log("");
212
+ console.log("%cSummary:", "font-weight: bold;");
213
+
214
+ const summary = hintTypes.map((type) => {
215
+ const count = allHints.filter((h) => h.rel === type.rel).length;
216
+ return { type: type.rel, count, icon: type.icon };
217
+ });
218
+
219
+ summary.forEach((s) => {
220
+ const status = s.count > 0 ? "🟢" : "⚪";
221
+ console.log(` ${status} ${s.icon} ${s.type}: ${s.count}`);
222
+ });
223
+
224
+ console.log(` Total hints: ${allHints.length}`);
225
+
226
+ // Details by type
227
+ hintTypes.forEach((type) => {
228
+ const hints = allHints.filter((h) => h.rel === type.rel);
229
+ if (hints.length === 0) return;
230
+
231
+ console.log("");
232
+ console.group(
233
+ `%c${type.icon} ${type.rel} (${hints.length})`,
234
+ "color: #3b82f6; font-weight: bold;"
235
+ );
236
+
237
+ const tableData = hints.map((h) => {
238
+ const row = {
239
+ Resource: h.shortHref,
240
+ };
241
+
242
+ if (type.rel === "preload" || type.rel === "modulepreload") {
243
+ row.As = h.as || "āš ļø missing";
244
+ row.Crossorigin = h.crossorigin || "-";
245
+ if (h.fetchpriority) row.Priority = h.fetchpriority;
246
+
247
+ // Check if used
248
+ const used = loadedResources.some((r) => r.name === h.href);
249
+ row.Used = used ? "āœ…" : "āš ļø No";
250
+ }
251
+
252
+ if (type.rel === "preconnect" || type.rel === "dns-prefetch") {
253
+ row.Origin = h.hostname;
254
+ row.Crossorigin = h.crossorigin || "-";
255
+
256
+ // Check resources from this origin
257
+ const originResources = loadedResources.filter((r) => {
258
+ try {
259
+ return new URL(r.name).hostname === h.hostname;
260
+ } catch {
261
+ return false;
262
+ }
263
+ });
264
+ row["Resources Loaded"] = originResources.length;
265
+ }
266
+
267
+ return row;
268
+ });
269
+
270
+ console.table(tableData);
271
+
272
+ // Elements for inspection
273
+ console.log("%cšŸ”Ž Elements:", "font-weight: bold;");
274
+ hints.forEach((h, i) => {
275
+ console.log(`${i + 1}.`, h.element);
276
+ });
277
+
278
+ console.groupEnd();
279
+ });
280
+
281
+ // Issues
282
+ const errors = issues.filter((i) => i.severity === "error");
283
+ const warnings = issues.filter((i) => i.severity === "warning");
284
+
285
+ if (issues.length > 0) {
286
+ console.log("");
287
+ console.group(
288
+ `%cāš ļø Issues Found (${errors.length} errors, ${warnings.length} warnings)`,
289
+ "color: #ef4444; font-weight: bold;"
290
+ );
291
+
292
+ if (errors.length > 0) {
293
+ console.log("");
294
+ console.log("%cšŸ”“ Errors:", "font-weight: bold; color: #ef4444;");
295
+ errors.forEach((issue) => {
296
+ console.log(` ${issue.hint.rel}: ${issue.issue}`);
297
+ console.log(` Resource: ${issue.hint.href}`);
298
+ console.log(` Fix: ${issue.fix}`);
299
+ });
300
+ }
301
+
302
+ if (warnings.length > 0) {
303
+ console.log("");
304
+ console.log("%c🟔 Warnings:", "font-weight: bold; color: #f59e0b;");
305
+ warnings.forEach((issue) => {
306
+ console.log(` ${issue.hint.rel}: ${issue.issue}`);
307
+ if (issue.hint.href) {
308
+ console.log(` Resource: ${issue.hint.href}`);
309
+ }
310
+ console.log(` Fix: ${issue.fix}`);
311
+ });
312
+ }
313
+
314
+ console.groupEnd();
315
+ } else {
316
+ console.log("");
317
+ console.log(
318
+ "%cāœ… No issues found with resource hints.",
319
+ "color: #22c55e; font-weight: bold;"
320
+ );
321
+ }
322
+
323
+ // Recommendations
324
+ if (missingPreconnects.length > 0) {
325
+ console.log("");
326
+ console.group("%cšŸ’” Optimization Opportunities", "color: #3b82f6; font-weight: bold;");
327
+
328
+ console.log("");
329
+ console.log("%cThird-party origins without preconnect:", "font-weight: bold;");
330
+ console.log("These origins load multiple resources and could benefit from preconnect:");
331
+ console.log("");
332
+
333
+ const recTable = missingPreconnects
334
+ .sort((a, b) => b.count - a.count)
335
+ .slice(0, 5)
336
+ .map((o) => ({
337
+ Origin: o.origin,
338
+ Resources: o.count,
339
+ Size: formatBytes(o.size),
340
+ Types: Array.from(o.types).join(", "),
341
+ }));
342
+ console.table(recTable);
343
+
344
+ console.log("");
345
+ console.log("%cAdd preconnect for critical origins:", "font-weight: bold;");
346
+ missingPreconnects.slice(0, 3).forEach((o) => {
347
+ console.log(
348
+ `%c<link rel="preconnect" href="${o.origin}" crossorigin>`,
349
+ "font-family: monospace; color: #22c55e;"
350
+ );
351
+ });
352
+
353
+ console.groupEnd();
354
+ }
355
+
356
+ // Best practices
357
+ console.log("");
358
+ console.group("%cšŸ“ Best Practices", "color: #3b82f6; font-weight: bold;");
359
+ console.log("");
360
+ console.log("%cPreload (current page critical resources):", "font-weight: bold;");
361
+ console.log(
362
+ '%c<link rel="preload" href="/font.woff2" as="font" type="font/woff2" crossorigin>',
363
+ "font-family: monospace; color: #22c55e;"
364
+ );
365
+ console.log(
366
+ '%c<link rel="preload" href="/hero.webp" as="image" fetchpriority="high">',
367
+ "font-family: monospace; color: #22c55e;"
368
+ );
369
+ console.log("");
370
+ console.log("%cPreconnect (third-party origins):", "font-weight: bold;");
371
+ console.log(
372
+ '%c<link rel="preconnect" href="https://fonts.googleapis.com">',
373
+ "font-family: monospace; color: #22c55e;"
374
+ );
375
+ console.log(
376
+ '%c<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>',
377
+ "font-family: monospace; color: #22c55e;"
378
+ );
379
+ console.log("");
380
+ console.log("%cPrefetch (next page resources):", "font-weight: bold;");
381
+ console.log(
382
+ '%c<link rel="prefetch" href="/next-page.js" as="script">',
383
+ "font-family: monospace; color: #22c55e;"
384
+ );
385
+ console.groupEnd();
386
+
387
+ console.groupEnd();
388
+
389
+ return {
390
+ script: "Resource-Hints",
391
+ status: "ok",
392
+ count: allHints.length,
393
+ details: {
394
+ byType: Object.fromEntries(hintTypes.map(t => [t.rel, allHints.filter(h => h.rel === t.rel).length])),
395
+ missingPreconnectsCount: missingPreconnects.length,
396
+ },
397
+ items: allHints.map(h => ({ rel: h.rel, href: h.shortHref, as: h.as, crossorigin: h.crossorigin, fetchpriority: h.fetchpriority })),
398
+ issues: issues.map(i => ({ severity: i.severity, message: `${i.hint.rel}: ${i.issue}. Fix: ${i.fix}` })),
399
+ };
400
+ })();