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,503 @@
1
+ // Resource Hints Validation
2
+ // https://webperf-snippets.nucliweb.net
3
+
4
+ (() => {
5
+ console.group("%c🔍 Resource Hints Validation", "font-weight: bold; font-size: 14px;");
6
+
7
+ // Check if page is fully loaded
8
+ if (document.readyState !== 'complete') {
9
+ console.log("");
10
+ console.log("%c⚠️ Warning: Page is still loading (readyState: " + document.readyState + ")", "color: #f59e0b; font-weight: bold;");
11
+ console.log("%cFor accurate results, wait until the page is fully loaded and run again.", "color: #6b7280;");
12
+ console.log("");
13
+ }
14
+
15
+ // Get all resource hints from DOM
16
+ const preloadLinks = Array.from(document.querySelectorAll('link[rel="preload"]'));
17
+ const preconnectLinks = Array.from(document.querySelectorAll('link[rel="preconnect"]'));
18
+ const dnsPrefetchLinks = Array.from(document.querySelectorAll('link[rel="dns-prefetch"]'));
19
+
20
+ // Get all loaded resources from Performance API
21
+ const resources = performance.getEntriesByType("resource");
22
+
23
+ // Helper: Extract domain from URL
24
+ const getDomain = (url) => {
25
+ try {
26
+ // Handle relative URLs by resolving against current location
27
+ const absoluteUrl = url.startsWith('http') ? url : new URL(url, location.href).href;
28
+ return new URL(absoluteUrl).origin;
29
+ } catch {
30
+ return null;
31
+ }
32
+ };
33
+
34
+ // Helper: Normalize URL for comparison
35
+ const normalizeUrl = (url) => {
36
+ try {
37
+ const parsed = new URL(url, location.origin);
38
+ return parsed.origin + parsed.pathname + parsed.search;
39
+ } catch {
40
+ return url;
41
+ }
42
+ };
43
+
44
+ // Helper: Format size
45
+ const formatSize = (bytes) => {
46
+ if (!bytes) return "N/A";
47
+ const kb = bytes / 1024;
48
+ return kb.toFixed(1) + " KB";
49
+ };
50
+
51
+ // Create maps of used resources and domains
52
+ const usedResourceUrls = new Set(resources.map(r => normalizeUrl(r.name)));
53
+ const usedDomains = new Set(resources.map(r => getDomain(r.name)).filter(Boolean));
54
+ usedDomains.add(location.origin);
55
+
56
+ // Get all image elements once for reuse across validations
57
+ const allImgElements = document.querySelectorAll('img[src]');
58
+
59
+ console.log("");
60
+ console.log("%cSummary:", "font-weight: bold;");
61
+ console.log(" Preload hints: " + preloadLinks.length);
62
+ console.log(" Preconnect hints: " + preconnectLinks.length);
63
+ console.log(" DNS-prefetch hints: " + dnsPrefetchLinks.length);
64
+ console.log(" Total resources loaded: " + resources.length);
65
+ console.log(" Unique domains accessed: " + usedDomains.size);
66
+
67
+ // ===== PRELOAD VALIDATION =====
68
+ console.log("");
69
+ console.group("%c1. Preload Validation", "font-weight: bold; color: #3b82f6;");
70
+
71
+ if (preloadLinks.length === 0) {
72
+ console.log("%cℹ️ No preload hints found.", "color: #6b7280;");
73
+ } else {
74
+ const preloadResults = preloadLinks.map(link => {
75
+ const href = link.href;
76
+ const as = link.getAttribute("as") || "unknown";
77
+ const media = link.getAttribute("media");
78
+ const normalizedUrl = normalizeUrl(href);
79
+ const shortUrl = href.split("/").pop()?.split("?").shift() || href;
80
+
81
+ const wasUsed = usedResourceUrls.has(normalizedUrl);
82
+ const perfEntry = resources.find(r => normalizeUrl(r.name) === normalizedUrl);
83
+
84
+ let mediaMatches = true;
85
+ if (media) {
86
+ try {
87
+ mediaMatches = window.matchMedia(media).matches;
88
+ } catch {
89
+ mediaMatches = false;
90
+ }
91
+ }
92
+
93
+ return {
94
+ shortUrl,
95
+ href,
96
+ as,
97
+ media,
98
+ mediaMatches,
99
+ wasUsed,
100
+ size: perfEntry ? formatSize(perfEntry.decodedBodySize || perfEntry.transferSize) : "N/A",
101
+ link
102
+ };
103
+ });
104
+
105
+ const usedPreloads = preloadResults.filter(r => r.wasUsed);
106
+ const unusedPreloads = preloadResults.filter(r => !r.wasUsed && (!r.media || r.mediaMatches));
107
+ const conditionalPreloads = preloadResults.filter(r => !r.wasUsed && r.media && !r.mediaMatches);
108
+
109
+ if (unusedPreloads.length > 0) {
110
+ console.log("");
111
+ console.log("%c❌ Unused Preloads (" + unusedPreloads.length + ")", "color: #ef4444; font-weight: bold;");
112
+ console.table(unusedPreloads.map(r => ({
113
+ Resource: r.shortUrl,
114
+ Type: r.as,
115
+ Status: "Not Used ❌"
116
+ })));
117
+ }
118
+
119
+ if (conditionalPreloads.length > 0) {
120
+ console.log("");
121
+ console.log("%cℹ️ Conditional Preloads (" + conditionalPreloads.length + ")", "color: #3b82f6; font-weight: bold;");
122
+ console.log("%cThese preloads have media queries that don't match current viewport (this is OK):", "color: #6b7280;");
123
+ console.table(conditionalPreloads.map(r => ({
124
+ Resource: r.shortUrl,
125
+ Type: r.as,
126
+ Media: r.media,
127
+ Status: "Conditional ✅"
128
+ })));
129
+ }
130
+
131
+ if (usedPreloads.length > 0) {
132
+ console.log("");
133
+ console.log("%c✅ Used Preloads (" + usedPreloads.length + ")", "color: #22c55e; font-weight: bold;");
134
+ console.table(usedPreloads.map(r => ({
135
+ Resource: r.shortUrl,
136
+ Type: r.as,
137
+ Size: r.size,
138
+ Status: "Used ✅"
139
+ })));
140
+ }
141
+
142
+ const activePreloads = preloadLinks.length - conditionalPreloads.length;
143
+ const usageRate = activePreloads > 0 ? Math.round((usedPreloads.length / activePreloads) * 100) : 0;
144
+ console.log("");
145
+ console.log("%cUsage Rate: " + usageRate + "%", "font-weight: bold;");
146
+
147
+ if (unusedPreloads.length === 0) {
148
+ console.log("%c✅ All applicable preload hints are being used.", "color: #22c55e;");
149
+ } else {
150
+ console.log("%c⚠️ " + unusedPreloads.length + " unused preload(s) waste bandwidth.", "color: #ef4444;");
151
+ }
152
+ }
153
+
154
+ console.groupEnd();
155
+
156
+ // ===== PRECONNECT VALIDATION =====
157
+ console.log("");
158
+ console.group("%c2. Preconnect Validation", "font-weight: bold; color: #3b82f6;");
159
+
160
+ // Get lazy-loaded image domains for preconnect validation
161
+ const lazyLoadedDomains = new Set();
162
+ allImgElements.forEach(img => {
163
+ try {
164
+ const domain = getDomain(img.src);
165
+ if (domain && domain !== location.origin && !usedDomains.has(domain)) {
166
+ lazyLoadedDomains.add(domain);
167
+ }
168
+ } catch {}
169
+ });
170
+
171
+ if (preconnectLinks.length === 0) {
172
+ console.log("%cℹ️ No preconnect hints found.", "color: #6b7280;");
173
+ } else {
174
+ const preconnectResults = preconnectLinks.map(link => {
175
+ const href = link.href;
176
+ const domain = getDomain(href);
177
+ const wasUsed = domain ? usedDomains.has(domain) : false;
178
+ const isLazyLoaded = domain ? lazyLoadedDomains.has(domain) : false;
179
+ const requestCount = resources.filter(r => getDomain(r.name) === domain).length;
180
+
181
+ return { domain, href, wasUsed, isLazyLoaded, requestCount, link };
182
+ });
183
+
184
+ const usedPreconnects = preconnectResults.filter(r => r.wasUsed);
185
+ const lazyLoadedPreconnects = preconnectResults.filter(r => !r.wasUsed && r.isLazyLoaded);
186
+ const unusedPreconnects = preconnectResults.filter(r => !r.wasUsed && !r.isLazyLoaded);
187
+
188
+ if (unusedPreconnects.length > 0) {
189
+ console.log("");
190
+ console.log("%c❌ Unused Preconnects (" + unusedPreconnects.length + ")", "color: #ef4444; font-weight: bold;");
191
+ console.table(unusedPreconnects.map(r => ({
192
+ Domain: r.domain || r.href,
193
+ Requests: 0,
194
+ Status: "Not Used ❌"
195
+ })));
196
+ }
197
+
198
+ if (lazyLoadedPreconnects.length > 0) {
199
+ console.log("");
200
+ console.log("%c✅ Lazy-Loaded Preconnects (" + lazyLoadedPreconnects.length + ")", "color: #22c55e; font-weight: bold;");
201
+ console.log("%cThese preconnects are for lazy-loaded resources (correct usage!):", "color: #6b7280;");
202
+ console.table(lazyLoadedPreconnects.map(r => ({
203
+ Domain: r.domain || r.href,
204
+ "Images in DOM": document.querySelectorAll('img[src*="' + (r.domain || r.href).replace(/https?:\/\//, '') + '"]').length,
205
+ Status: "Lazy-Loaded ✅"
206
+ })));
207
+ }
208
+
209
+ if (usedPreconnects.length > 0) {
210
+ console.log("");
211
+ console.log("%c✅ Used Preconnects (" + usedPreconnects.length + ")", "color: #22c55e; font-weight: bold;");
212
+ console.table(usedPreconnects.map(r => ({
213
+ Domain: r.domain || r.href,
214
+ Requests: r.requestCount,
215
+ Status: "Used ✅"
216
+ })));
217
+ }
218
+
219
+ const effectivePreconnects = usedPreconnects.length + lazyLoadedPreconnects.length;
220
+ const usageRate = Math.round((effectivePreconnects / preconnectLinks.length) * 100);
221
+ console.log("");
222
+ console.log("%cUsage Rate: " + usageRate + "%", "font-weight: bold;");
223
+
224
+ if (unusedPreconnects.length === 0) {
225
+ console.log("%c✅ All preconnect hints are being used.", "color: #22c55e;");
226
+ } else {
227
+ console.log("%c⚠️ " + unusedPreconnects.length + " unused preconnect(s) waste CPU/network.", "color: #ef4444;");
228
+ }
229
+ }
230
+
231
+ console.groupEnd();
232
+
233
+ // ===== DNS-PREFETCH VALIDATION =====
234
+ console.log("");
235
+ console.group("%c3. DNS-Prefetch Validation", "font-weight: bold; color: #3b82f6;");
236
+
237
+ if (dnsPrefetchLinks.length === 0) {
238
+ console.log("%cℹ️ No dns-prefetch hints found.", "color: #6b7280;");
239
+ } else {
240
+ const dnsPrefetchResults = dnsPrefetchLinks.map(link => {
241
+ const href = link.href;
242
+ const domain = getDomain(href);
243
+ const wasUsed = domain ? usedDomains.has(domain) : false;
244
+ const isLazyLoaded = domain ? lazyLoadedDomains.has(domain) : false;
245
+ const requestCount = resources.filter(r => getDomain(r.name) === domain).length;
246
+
247
+ return { domain, href, wasUsed, isLazyLoaded, requestCount, link };
248
+ });
249
+
250
+ const usedDnsPrefetch = dnsPrefetchResults.filter(r => r.wasUsed);
251
+ const lazyLoadedDnsPrefetch = dnsPrefetchResults.filter(r => !r.wasUsed && r.isLazyLoaded);
252
+ const unusedDnsPrefetch = dnsPrefetchResults.filter(r => !r.wasUsed && !r.isLazyLoaded);
253
+
254
+ if (unusedDnsPrefetch.length > 0) {
255
+ console.log("");
256
+ console.log("%cℹ️ Unused DNS-Prefetch (" + unusedDnsPrefetch.length + ")", "color: #f59e0b; font-weight: bold;");
257
+ console.table(unusedDnsPrefetch.map(r => ({
258
+ Domain: r.domain || r.href,
259
+ Requests: 0,
260
+ Status: "Not Used"
261
+ })));
262
+ console.log("%cNote: Unused dns-prefetch has minimal impact.", "color: #6b7280;");
263
+ }
264
+
265
+ if (lazyLoadedDnsPrefetch.length > 0) {
266
+ console.log("");
267
+ console.log("%c✅ Lazy-Loaded DNS-Prefetch (" + lazyLoadedDnsPrefetch.length + ")", "color: #22c55e; font-weight: bold;");
268
+ console.log("%cThese dns-prefetch hints are for lazy-loaded resources (correct usage!):", "color: #6b7280;");
269
+ console.table(lazyLoadedDnsPrefetch.map(r => ({
270
+ Domain: r.domain || r.href,
271
+ "Images in DOM": document.querySelectorAll('img[src*="' + (r.domain || r.href).replace(/https?:\/\//, '') + '"]').length,
272
+ Status: "Lazy-Loaded ✅"
273
+ })));
274
+ }
275
+
276
+ if (usedDnsPrefetch.length > 0) {
277
+ console.log("");
278
+ console.log("%c✅ Used DNS-Prefetch (" + usedDnsPrefetch.length + ")", "color: #22c55e; font-weight: bold;");
279
+ console.table(usedDnsPrefetch.map(r => ({
280
+ Domain: r.domain || r.href,
281
+ Requests: r.requestCount,
282
+ Status: "Used ✅"
283
+ })));
284
+ }
285
+
286
+ const effectiveDnsPrefetch = usedDnsPrefetch.length + lazyLoadedDnsPrefetch.length;
287
+ const usageRate = Math.round((effectiveDnsPrefetch / dnsPrefetchLinks.length) * 100);
288
+ console.log("");
289
+ console.log("%cUsage Rate: " + usageRate + "%", "font-weight: bold;");
290
+ }
291
+
292
+ console.groupEnd();
293
+
294
+ // ===== REDUNDANT HINTS CHECK =====
295
+ console.log("");
296
+ console.group("%c⚠️ Redundant Hints", "font-weight: bold; color: #f59e0b;");
297
+
298
+ const preconnectDomains = new Set(preconnectLinks.map(l => getDomain(l.href)).filter(Boolean));
299
+ const dnsPrefetchDomains = new Set(dnsPrefetchLinks.map(l => getDomain(l.href)).filter(Boolean));
300
+ const redundantDomains = Array.from(preconnectDomains).filter(d => dnsPrefetchDomains.has(d));
301
+
302
+ if (redundantDomains.length > 0) {
303
+ console.log("");
304
+ console.log("%c⚠️ Redundant Configuration Detected", "color: #f59e0b; font-weight: bold;");
305
+ console.log("%cThese domains have BOTH preconnect AND dns-prefetch hints:", "color: #6b7280;");
306
+ console.log("");
307
+ redundantDomains.forEach(domain => {
308
+ console.log(" • " + domain);
309
+ });
310
+ console.log("");
311
+ console.log("%c💡 Recommendation: Remove dns-prefetch for these domains.", "color: #3b82f6; font-weight: bold;");
312
+ console.log("%cpreconnect already includes DNS resolution + TCP + TLS handshake.", "color: #6b7280;");
313
+ console.log("%cdns-prefetch only does DNS resolution, so it's redundant when preconnect exists.", "color: #6b7280;");
314
+ } else {
315
+ console.log("%c✅ No redundant hints found.", "color: #22c55e;");
316
+ }
317
+
318
+ console.groupEnd();
319
+
320
+ // ===== PERFORMANCE API LIMITATIONS CHECK =====
321
+ console.log("");
322
+ console.group("%c⚠️ Performance API Limitations", "font-weight: bold; color: #f59e0b;");
323
+
324
+ // Check for images in DOM that might not be in Performance API
325
+ const imgDomains = new Set();
326
+
327
+ allImgElements.forEach(img => {
328
+ try {
329
+ const domain = getDomain(img.src);
330
+ if (domain && domain !== location.origin) {
331
+ imgDomains.add(domain);
332
+ }
333
+ } catch {}
334
+ });
335
+
336
+ const missingFromPerf = Array.from(imgDomains).filter(domain => !usedDomains.has(domain));
337
+
338
+ if (missingFromPerf.length > 0) {
339
+ console.log("");
340
+ console.log("%c⚠️ Lazy-Loaded Resources Detected", "color: #f59e0b; font-weight: bold;");
341
+ console.log("%cThese domains have resources in the DOM but not in Performance API.", "color: #6b7280;");
342
+ console.log("%cThis typically happens with lazy-loaded images or service worker cached resources.", "color: #6b7280;");
343
+ console.log("");
344
+ console.log("%cDomains with lazy-loaded resources:", "font-weight: bold;");
345
+
346
+ const domainsWithCounts = missingFromPerf
347
+ .map(domain => {
348
+ const imgCount = Array.from(allImgElements).filter(img => {
349
+ try {
350
+ return getDomain(img.src) === domain;
351
+ } catch {
352
+ return false;
353
+ }
354
+ }).length;
355
+ return { domain, imgCount };
356
+ })
357
+ .filter(item => item.domain && item.domain !== 'null')
358
+ .sort((a, b) => b.imgCount - a.imgCount);
359
+
360
+ domainsWithCounts.forEach(item => {
361
+ console.log(" • " + item.domain + " (" + item.imgCount + " images in DOM)");
362
+ });
363
+
364
+ console.log("");
365
+ console.log("%c✅ IMPORTANT: Keep preconnect/dns-prefetch hints for these domains!", "color: #22c55e; font-weight: bold;");
366
+ console.log("%cLazy-loaded resources benefit MORE from connection hints because the connection", "color: #6b7280;");
367
+ console.log("%cis established early, making images load instantly when the user scrolls to them.", "color: #6b7280;");
368
+ console.log("");
369
+ console.log("%cIf these domains have preconnect hints marked as 'unused' above, IGNORE that warning.", "color: #f59e0b; font-weight: bold;");
370
+ console.log("%cThe hints are working correctly - the Performance API just can't see lazy-loaded resources.", "color: #6b7280;");
371
+ } else {
372
+ console.log("%c✅ All DOM image domains are captured in Performance API.", "color: #22c55e;");
373
+ }
374
+
375
+ console.groupEnd();
376
+
377
+ // ===== OPPORTUNITIES =====
378
+ console.log("");
379
+ console.group("%c4. Optimization Opportunities", "font-weight: bold; color: #3b82f6;");
380
+
381
+ const hintedDomains = new Set([
382
+ ...preconnectLinks.map(l => getDomain(l.href)),
383
+ ...dnsPrefetchLinks.map(l => getDomain(l.href))
384
+ ]);
385
+
386
+ const domainRequestCounts = {};
387
+ resources.forEach(r => {
388
+ const domain = getDomain(r.name);
389
+ if (domain && domain !== location.origin) {
390
+ domainRequestCounts[domain] = (domainRequestCounts[domain] || 0) + 1;
391
+ }
392
+ });
393
+
394
+ const missingHints = Object.entries(domainRequestCounts)
395
+ .filter(([domain, count]) => count >= 2 && !hintedDomains.has(domain))
396
+ .sort((a, b) => b[1] - a[1]);
397
+
398
+ const topPreconnects = missingHints.filter(([, count]) => count >= 5).slice(0, 3);
399
+ const topDnsPrefetch = missingHints.filter(([, count]) => count >= 2 && count < 5).slice(0, 5);
400
+ const shownHints = [...topPreconnects, ...topDnsPrefetch];
401
+
402
+ if (missingHints.length > 0) {
403
+ console.log("");
404
+ console.log("%c⚡ Optimization Opportunities (" + missingHints.length + " found)", "color: #f59e0b; font-weight: bold;");
405
+ console.log("%cThese domains make multiple requests but have no connection hints:", "color: #6b7280;");
406
+
407
+ if (shownHints.length < missingHints.length) {
408
+ console.log("%c⚠️ Showing top " + shownHints.length + " recommendations", "color: #f59e0b;");
409
+ console.log("%c💡 Too many hints compete for bandwidth. Focus on the most critical domains.", "color: #6b7280;");
410
+ }
411
+
412
+ console.table(shownHints.map(([domain, count]) => ({
413
+ Domain: domain,
414
+ Requests: count,
415
+ Recommendation: count >= 5 ? "preconnect" : "dns-prefetch",
416
+ Priority: count >= 10 ? "High" : count >= 5 ? "Medium" : "Low"
417
+ })));
418
+
419
+ console.log("");
420
+ console.log("%cSuggested code:", "font-weight: bold;");
421
+ shownHints.forEach(([domain, count]) => {
422
+ const hint = count >= 5 ? "preconnect" : "dns-prefetch";
423
+ console.log('%c<link rel="' + hint + '" href="' + domain + '">', "font-family: monospace;");
424
+ });
425
+ } else {
426
+ console.log("%c✅ All frequently-used domains have appropriate hints.", "color: #22c55e;");
427
+ }
428
+
429
+ console.groupEnd();
430
+
431
+ // ===== BEST PRACTICES =====
432
+ console.log("");
433
+ console.group("%c📝 Best Practices", "color: #3b82f6; font-weight: bold;");
434
+ console.log("");
435
+ console.log("%cPreload:", "font-weight: bold;");
436
+ console.log(" ✅ Use for critical resources on current page");
437
+ console.log(" ❌ Don't preload resources not used on current page");
438
+ console.log(" ⚡ Limit to few most critical resources");
439
+ console.log("");
440
+ console.log("%cPreconnect:", "font-weight: bold;");
441
+ console.log(" ✅ Use for critical third-party domains");
442
+ console.log(" ✅ Best for domains with many requests");
443
+ console.log(" ❌ Don't use for domains not accessed");
444
+ console.log(" ⚡ Limit to few most important domains");
445
+ console.log("");
446
+ console.log("%cDNS-Prefetch:", "font-weight: bold;");
447
+ console.log(" ✅ Use for non-critical third-party domains");
448
+ console.log(" ✅ Good for domains with few requests");
449
+ console.log(" ℹ️ Less impact than preconnect");
450
+
451
+ console.groupEnd();
452
+
453
+ // Final summary - reuse lazy-loaded domains set from earlier
454
+ const lazyLoadedDomainsForSummary = new Set();
455
+ allImgElements.forEach(img => {
456
+ try {
457
+ const domain = getDomain(img.src);
458
+ if (domain && domain !== location.origin && !usedDomains.has(domain)) {
459
+ lazyLoadedDomainsForSummary.add(domain);
460
+ }
461
+ } catch {}
462
+ });
463
+
464
+ const totalIssues = preloadLinks.filter(l => !usedResourceUrls.has(normalizeUrl(l.href))).length +
465
+ preconnectLinks.filter(l => {
466
+ const domain = getDomain(l.href);
467
+ return domain && !usedDomains.has(domain) && !lazyLoadedDomainsForSummary.has(domain);
468
+ }).length +
469
+ dnsPrefetchLinks.filter(l => {
470
+ const domain = getDomain(l.href);
471
+ return domain && !usedDomains.has(domain) && !lazyLoadedDomainsForSummary.has(domain);
472
+ }).length;
473
+
474
+ console.log("");
475
+ if (totalIssues === 0 && missingHints.length === 0) {
476
+ console.log("%c✅ Excellent! All resource hints are properly configured.", "color: #22c55e; font-weight: bold; font-size: 14px;");
477
+ } else {
478
+ console.log("%c⚠️ Found " + totalIssues + " unused hint(s) and " + missingHints.length + " optimization opportunity(ies).", "color: #ef4444; font-weight: bold;");
479
+ console.log("%cReview recommendations above to improve performance.", "color: #6b7280;");
480
+ }
481
+
482
+ console.groupEnd();
483
+
484
+ return {
485
+ script: "Resource-Hints-Validation",
486
+ status: "ok",
487
+ count: preloadLinks.length + preconnectLinks.length + dnsPrefetchLinks.length,
488
+ details: {
489
+ preloadCount: preloadLinks.length,
490
+ preconnectCount: preconnectLinks.length,
491
+ dnsPrefetchCount: dnsPrefetchLinks.length,
492
+ totalResourcesLoaded: resources.length,
493
+ unusedHints: totalIssues,
494
+ missingPreconnects: missingHints.length,
495
+ redundantHints: redundantDomains.length,
496
+ },
497
+ issues: [
498
+ ...(totalIssues > 0 ? [{ severity: "warning", message: `${totalIssues} unused hint(s) waste bandwidth` }] : []),
499
+ ...(missingHints.length > 0 ? [{ severity: "info", message: `${missingHints.length} domain(s) could benefit from connection hints` }] : []),
500
+ ...(redundantDomains.length > 0 ? [{ severity: "warning", message: `${redundantDomains.length} domain(s) have both preconnect and dns-prefetch (redundant)` }] : []),
501
+ ],
502
+ };
503
+ })();