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.
- package/README.md +139 -0
- package/package.json +54 -0
- package/snippets/CoreWebVitals/CLS.js +88 -0
- package/snippets/CoreWebVitals/INP.js +432 -0
- package/snippets/CoreWebVitals/LCP-Image-Entropy.js +198 -0
- package/snippets/CoreWebVitals/LCP-Subparts.js +286 -0
- package/snippets/CoreWebVitals/LCP-Trail.js +184 -0
- package/snippets/CoreWebVitals/LCP-Video-Candidate.js +228 -0
- package/snippets/CoreWebVitals/LCP.js +135 -0
- package/snippets/CoreWebVitals/WORKFLOWS.md +203 -0
- package/snippets/DevTools-Overrides/Fetch-XHR-Timeline-inject.js +73 -0
- package/snippets/DevTools-Overrides/Fetch-XHR-Timeline-read.js +191 -0
- package/snippets/Interaction/Forced-Synchronous-Layout.js +286 -0
- package/snippets/Interaction/Input-Latency-Breakdown.js +199 -0
- package/snippets/Interaction/Interactions.js +220 -0
- package/snippets/Interaction/Layout-Shift-Loading-and-Interaction.js +266 -0
- package/snippets/Interaction/Long-Animation-Frames-Helpers.js +397 -0
- package/snippets/Interaction/Long-Animation-Frames-Script-Attribution.js +137 -0
- package/snippets/Interaction/Long-Animation-Frames.js +298 -0
- package/snippets/Interaction/LongTask.js +203 -0
- package/snippets/Interaction/Scroll-Performance.js +359 -0
- package/snippets/Interaction/WORKFLOWS.md +205 -0
- package/snippets/Loading/Back-Forward-Cache.js +473 -0
- package/snippets/Loading/CSS-Media-Queries-Analysis.js +512 -0
- package/snippets/Loading/Cache-Strategy-Analysis.js +808 -0
- package/snippets/Loading/Client-Side-Redirect-Detection.js +337 -0
- package/snippets/Loading/Content-Visibility.js +352 -0
- package/snippets/Loading/Critical-CSS-Detection.js +329 -0
- package/snippets/Loading/Event-Processing-Time.js +179 -0
- package/snippets/Loading/FCP.js +101 -0
- package/snippets/Loading/Find-Above-The-Fold-Lazy-Loaded-Images.js +264 -0
- package/snippets/Loading/Find-Images-With-Lazy-and-Fetchpriority.js +155 -0
- package/snippets/Loading/Find-non-Lazy-Loaded-Images-outside-of-the-viewport.js +300 -0
- package/snippets/Loading/Find-render-blocking-resources.js +161 -0
- package/snippets/Loading/First-And-Third-Party-Script-Info.js +269 -0
- package/snippets/Loading/First-And-Third-Party-Script-Timings.js +295 -0
- package/snippets/Loading/Fonts-Preloaded-Loaded-and-used-above-the-fold.js +317 -0
- package/snippets/Loading/Inline-CSS-Info-and-Size.js +234 -0
- package/snippets/Loading/Inline-Script-Info-and-Size.js +323 -0
- package/snippets/Loading/JS-Execution-Time-Breakdown.js +336 -0
- package/snippets/Loading/Prefetch-Resource-Validation.js +456 -0
- package/snippets/Loading/Priority-Hints-Audit.js +336 -0
- package/snippets/Loading/Resource-Hints-Validation.js +503 -0
- package/snippets/Loading/Resource-Hints.js +400 -0
- package/snippets/Loading/SSR-Hydration-Data-Analysis.js +364 -0
- package/snippets/Loading/Script-Loading.js +332 -0
- package/snippets/Loading/Service-Worker-Analysis.js +192 -0
- package/snippets/Loading/TTFB-Resources.js +115 -0
- package/snippets/Loading/TTFB-Sub-Parts.js +117 -0
- package/snippets/Loading/TTFB.js +37 -0
- package/snippets/Loading/Validate-Preload-Async-Defer-Scripts.js +394 -0
- package/snippets/Loading/WORKFLOWS.md +163 -0
- package/snippets/Media/Image-Element-Audit.js +350 -0
- package/snippets/Media/SVG-Embedded-Bitmap-Analysis.js +225 -0
- package/snippets/Media/Video-Element-Audit.js +282 -0
- package/snippets/Media/WORKFLOWS.md +190 -0
- package/snippets/README.md +98 -0
- package/snippets/Resources/Network-Bandwidth-Connection-Quality.js +103 -0
- package/snippets/Resources/WORKFLOWS.md +173 -0
- package/src/bin.js +229 -0
- package/src/decision-tree.js +19 -0
- package/src/load-snippet.js +23 -0
- package/src/reporters/human.js +179 -0
- package/src/reporters/json.js +3 -0
- package/src/runner.js +61 -0
- package/src/workflows/audit.js +21 -0
- package/src/workflows/cwv.js +10 -0
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
// Prefetch Resource Validation
|
|
2
|
+
// https://webperf-snippets.nucliweb.net
|
|
3
|
+
|
|
4
|
+
(() => {
|
|
5
|
+
// Get all prefetch hints
|
|
6
|
+
const prefetchLinks = Array.from(document.querySelectorAll('link[rel="prefetch"]'));
|
|
7
|
+
|
|
8
|
+
if (prefetchLinks.length === 0) {
|
|
9
|
+
// Early return is safe here - console.group hasn't been opened yet
|
|
10
|
+
console.log(
|
|
11
|
+
'%c✅ No prefetch hints found (rel="prefetch").',
|
|
12
|
+
"color: #22c55e; font-weight: bold;",
|
|
13
|
+
);
|
|
14
|
+
console.log(
|
|
15
|
+
"%cℹ️ Prefetch is for future navigation resources. Use it sparingly for predictable user journeys.",
|
|
16
|
+
"color: #3b82f6;",
|
|
17
|
+
);
|
|
18
|
+
return { script: "Prefetch-Resource-Validation", status: "ok", count: 0, items: [], issues: [] };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Get performance entries for all resources
|
|
22
|
+
const performanceEntries = performance.getEntriesByType("resource");
|
|
23
|
+
|
|
24
|
+
// Pre-normalize URLs for efficient matching (avoids repeated URL parsing in nested loops)
|
|
25
|
+
const entryUrlMap = new Map();
|
|
26
|
+
performanceEntries.forEach((entry) => {
|
|
27
|
+
try {
|
|
28
|
+
const normalizedUrl = new URL(entry.name, location.origin).href;
|
|
29
|
+
if (!entryUrlMap.has(normalizedUrl)) {
|
|
30
|
+
entryUrlMap.set(normalizedUrl, []);
|
|
31
|
+
}
|
|
32
|
+
entryUrlMap.get(normalizedUrl).push(entry);
|
|
33
|
+
} catch {
|
|
34
|
+
// Ignore malformed URLs in performance entries
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Valid 'as' attribute values for prefetch (per HTML spec)
|
|
39
|
+
const validAsValues = new Set([
|
|
40
|
+
"script",
|
|
41
|
+
"style",
|
|
42
|
+
"font",
|
|
43
|
+
"image",
|
|
44
|
+
"video",
|
|
45
|
+
"audio",
|
|
46
|
+
"document",
|
|
47
|
+
"fetch",
|
|
48
|
+
"track",
|
|
49
|
+
"worker",
|
|
50
|
+
// Note: "media" is not a valid spec value, use "video" or "audio" instead
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
// Thresholds
|
|
54
|
+
const THRESHOLDS = {
|
|
55
|
+
maxCount: 10,
|
|
56
|
+
largeFileSize: 500 * 1024, // 500KB
|
|
57
|
+
largeScriptSize: 1024 * 1024, // 1MB for scripts specifically
|
|
58
|
+
totalSizeWarning: 2 * 1024 * 1024, // 2MB
|
|
59
|
+
totalSizeCritical: 5 * 1024 * 1024, // 5MB
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Analyze each prefetch
|
|
63
|
+
const issues = [];
|
|
64
|
+
const validPrefetch = [];
|
|
65
|
+
const seenUrls = new Set(); // Track duplicates
|
|
66
|
+
let totalSize = 0;
|
|
67
|
+
let totalTransferSize = 0;
|
|
68
|
+
|
|
69
|
+
prefetchLinks.forEach((link) => {
|
|
70
|
+
const href = link.href;
|
|
71
|
+
const as = link.getAttribute("as") || "unknown";
|
|
72
|
+
// Extract filename from URL; fallback to full href if URL is just origin
|
|
73
|
+
// Note: Removes query params but preserves hash (e.g., page#section)
|
|
74
|
+
const shortUrl = href.split("/").pop()?.split("?")[0]?.split("#")[0] || href;
|
|
75
|
+
|
|
76
|
+
// Normalize URL for matching
|
|
77
|
+
let normalizedUrl;
|
|
78
|
+
try {
|
|
79
|
+
normalizedUrl = new URL(href, location.origin).href;
|
|
80
|
+
} catch {
|
|
81
|
+
// Invalid href - will be caught later
|
|
82
|
+
normalizedUrl = href;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Find matching performance entries using pre-built map
|
|
86
|
+
const matchingEntries = entryUrlMap.get(normalizedUrl) || [];
|
|
87
|
+
const perfEntry = matchingEntries[0]; // Use first matching entry
|
|
88
|
+
|
|
89
|
+
const analysis = {
|
|
90
|
+
link,
|
|
91
|
+
href,
|
|
92
|
+
shortUrl,
|
|
93
|
+
as,
|
|
94
|
+
size: 0,
|
|
95
|
+
transferSize: 0,
|
|
96
|
+
duration: 0,
|
|
97
|
+
loaded: false,
|
|
98
|
+
isCurrentPage: false,
|
|
99
|
+
warnings: [],
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Check for missing or invalid 'as' attribute
|
|
103
|
+
if (as === "unknown") {
|
|
104
|
+
analysis.warnings.push({
|
|
105
|
+
type: "missing-as",
|
|
106
|
+
severity: "warning",
|
|
107
|
+
message: "Missing 'as' attribute - browser cannot apply correct MIME type matching",
|
|
108
|
+
});
|
|
109
|
+
} else if (!validAsValues.has(as)) {
|
|
110
|
+
analysis.warnings.push({
|
|
111
|
+
type: "invalid-as",
|
|
112
|
+
severity: "warning",
|
|
113
|
+
message: `Invalid 'as' value: "${as}" - should be one of: ${Array.from(validAsValues).join(", ")}`,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Check for duplicate prefetch
|
|
118
|
+
if (seenUrls.has(normalizedUrl)) {
|
|
119
|
+
analysis.warnings.push({
|
|
120
|
+
type: "duplicate-prefetch",
|
|
121
|
+
severity: "warning",
|
|
122
|
+
message: "Duplicate prefetch - this URL is prefetched multiple times",
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
seenUrls.add(normalizedUrl);
|
|
126
|
+
|
|
127
|
+
// Check if resource was actually loaded
|
|
128
|
+
if (perfEntry) {
|
|
129
|
+
analysis.loaded = true;
|
|
130
|
+
analysis.size = perfEntry.decodedBodySize || 0;
|
|
131
|
+
analysis.transferSize = perfEntry.transferSize || 0;
|
|
132
|
+
analysis.duration = perfEntry.duration || 0;
|
|
133
|
+
|
|
134
|
+
// Cache status detection with CORS considerations
|
|
135
|
+
// transferSize === 0 can mean: (1) cached, or (2) cross-origin without Timing-Allow-Origin
|
|
136
|
+
if (perfEntry.encodedBodySize > 0 && perfEntry.transferSize === 0) {
|
|
137
|
+
analysis.cacheStatus = "cached";
|
|
138
|
+
} else if (perfEntry.encodedBodySize === 0 && perfEntry.transferSize === 0) {
|
|
139
|
+
// Both zero = likely CORS-blocked timing info
|
|
140
|
+
analysis.cacheStatus = "unknown (CORS)";
|
|
141
|
+
} else {
|
|
142
|
+
analysis.cacheStatus = "network";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
totalSize += analysis.size;
|
|
146
|
+
totalTransferSize += analysis.transferSize;
|
|
147
|
+
|
|
148
|
+
// Check if it's a current page resource
|
|
149
|
+
// Uses 5s heuristic: resources loaded in first 5s are likely for current page, not prefetch
|
|
150
|
+
// This cutoff works for most pages but may flag slow pages incorrectly
|
|
151
|
+
const isCurrentPageResource = matchingEntries.some(
|
|
152
|
+
(entry) => entry.initiatorType !== "link" && entry.startTime < 5000,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
if (isCurrentPageResource) {
|
|
156
|
+
analysis.isCurrentPage = true;
|
|
157
|
+
analysis.warnings.push({
|
|
158
|
+
type: "wrong-hint",
|
|
159
|
+
severity: "error",
|
|
160
|
+
message: "Resource used on current page - should use preload instead",
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Check file size
|
|
165
|
+
if (analysis.size > THRESHOLDS.largeFileSize) {
|
|
166
|
+
const sizeMB = (analysis.size / (1024 * 1024)).toFixed(2);
|
|
167
|
+
analysis.warnings.push({
|
|
168
|
+
type: "large-file",
|
|
169
|
+
severity: "warning",
|
|
170
|
+
message: `Large file (${sizeMB}MB) - consider if prefetch is appropriate`,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Check resource type appropriateness (size-based)
|
|
175
|
+
// Note: Only checks if resource has loaded (has perfEntry with size)
|
|
176
|
+
let isInappropriate = false;
|
|
177
|
+
let inappropriateReason = "";
|
|
178
|
+
|
|
179
|
+
if (as === "video" || as === "audio") {
|
|
180
|
+
isInappropriate = true;
|
|
181
|
+
inappropriateReason = "Video/audio files are typically too large for prefetch";
|
|
182
|
+
} else if (as === "image" && analysis.size > 200 * 1024) {
|
|
183
|
+
isInappropriate = true;
|
|
184
|
+
inappropriateReason = `Image is ${(analysis.size / 1024).toFixed(0)}KB (>200KB threshold)`;
|
|
185
|
+
} else if (as === "script" && analysis.size > THRESHOLDS.largeScriptSize) {
|
|
186
|
+
isInappropriate = true;
|
|
187
|
+
inappropriateReason = `Script is ${(analysis.size / 1024).toFixed(0)}KB (>1MB threshold)`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (isInappropriate) {
|
|
191
|
+
analysis.warnings.push({
|
|
192
|
+
type: "inappropriate-type",
|
|
193
|
+
severity: "warning",
|
|
194
|
+
message: `Type "${as}" may not be suitable for prefetch: ${inappropriateReason}`,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
// Prefetch defined but not loaded yet (or failed)
|
|
199
|
+
analysis.warnings.push({
|
|
200
|
+
type: "not-loaded",
|
|
201
|
+
severity: "info",
|
|
202
|
+
message: "Not loaded yet or failed to load",
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Categorize
|
|
207
|
+
if (analysis.warnings.length > 0) {
|
|
208
|
+
issues.push(analysis);
|
|
209
|
+
} else {
|
|
210
|
+
validPrefetch.push(analysis);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Check total count
|
|
215
|
+
const countIssues = [];
|
|
216
|
+
if (prefetchLinks.length > THRESHOLDS.maxCount) {
|
|
217
|
+
countIssues.push({
|
|
218
|
+
type: "excessive-count",
|
|
219
|
+
severity: "warning",
|
|
220
|
+
message: `${prefetchLinks.length} prefetch hints found (recommended: <${THRESHOLDS.maxCount})`,
|
|
221
|
+
explanation: "Excessive prefetch can waste bandwidth, especially on mobile networks",
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Check total size
|
|
226
|
+
const totalSizeMB = (totalSize / (1024 * 1024)).toFixed(2);
|
|
227
|
+
const totalTransferMB = (totalTransferSize / (1024 * 1024)).toFixed(2);
|
|
228
|
+
|
|
229
|
+
if (totalSize > THRESHOLDS.totalSizeCritical) {
|
|
230
|
+
countIssues.push({
|
|
231
|
+
type: "excessive-size",
|
|
232
|
+
severity: "error",
|
|
233
|
+
message: `Total prefetch size: ${totalSizeMB}MB (critical threshold exceeded)`,
|
|
234
|
+
explanation: "This is very high and will significantly impact mobile users",
|
|
235
|
+
});
|
|
236
|
+
} else if (totalSize > THRESHOLDS.totalSizeWarning) {
|
|
237
|
+
countIssues.push({
|
|
238
|
+
type: "high-size",
|
|
239
|
+
severity: "warning",
|
|
240
|
+
message: `Total prefetch size: ${totalSizeMB}MB (warning threshold exceeded)`,
|
|
241
|
+
explanation: "Consider reducing prefetch to improve mobile experience",
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Display results
|
|
246
|
+
console.group("%c🔍 Prefetch Resource Validation", "font-weight: bold; font-size: 14px;");
|
|
247
|
+
|
|
248
|
+
// Count actually loaded resources
|
|
249
|
+
const loadedCount = [...issues, ...validPrefetch].filter((item) => item.loaded).length;
|
|
250
|
+
|
|
251
|
+
// Count meaningful issues (exclude "info" severity)
|
|
252
|
+
const meaningfulIssues = issues.filter((i) =>
|
|
253
|
+
i.warnings.some((w) => w.severity === "error" || w.severity === "warning"),
|
|
254
|
+
);
|
|
255
|
+
const totalMeaningfulIssues = meaningfulIssues.length + countIssues.length;
|
|
256
|
+
|
|
257
|
+
console.log("");
|
|
258
|
+
console.log("%cSummary:", "font-weight: bold;");
|
|
259
|
+
console.log(` Total prefetch hints: ${prefetchLinks.length}`);
|
|
260
|
+
console.log(` Loaded resources: ${loadedCount}`);
|
|
261
|
+
console.log(` Total size: ${totalSizeMB} MB (decompressed)`);
|
|
262
|
+
console.log(` Transfer size: ${totalTransferMB} MB (over network)`);
|
|
263
|
+
console.log(` Issues found: ${totalMeaningfulIssues} (excluding "not loaded" info)`);
|
|
264
|
+
|
|
265
|
+
// Show count/size issues first
|
|
266
|
+
if (countIssues.length > 0) {
|
|
267
|
+
console.log("");
|
|
268
|
+
console.group(
|
|
269
|
+
`%c⚠️ Overall Issues (${countIssues.length})`,
|
|
270
|
+
"color: #ef4444; font-weight: bold;",
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
countIssues.forEach((issue) => {
|
|
274
|
+
const color = issue.severity === "error" ? "#ef4444" : "#f59e0b";
|
|
275
|
+
const icon = issue.severity === "error" ? "🔴" : "⚠️";
|
|
276
|
+
console.log("");
|
|
277
|
+
console.log(`%c${icon} ${issue.message}`, `color: ${color}; font-weight: bold;`);
|
|
278
|
+
console.log(` ${issue.explanation}`);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
console.groupEnd();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Show resource-specific issues
|
|
285
|
+
const errors = issues.filter((i) => i.warnings.some((w) => w.severity === "error"));
|
|
286
|
+
const warnings = issues.filter(
|
|
287
|
+
(i) =>
|
|
288
|
+
!i.warnings.some((w) => w.severity === "error") &&
|
|
289
|
+
i.warnings.some((w) => w.severity === "warning"),
|
|
290
|
+
);
|
|
291
|
+
const infos = issues.filter((i) => i.warnings.every((w) => w.severity === "info"));
|
|
292
|
+
|
|
293
|
+
if (errors.length > 0) {
|
|
294
|
+
console.log("");
|
|
295
|
+
console.group(`%c🔴 Critical Issues (${errors.length})`, "color: #ef4444; font-weight: bold;");
|
|
296
|
+
|
|
297
|
+
const errorTable = errors.map((item) => ({
|
|
298
|
+
Resource: item.shortUrl,
|
|
299
|
+
Type: item.as,
|
|
300
|
+
Size: item.size > 0 ? `${(item.size / 1024).toFixed(1)} KB` : "Unknown",
|
|
301
|
+
Duration: item.duration > 0 ? `${item.duration.toFixed(0)} ms` : "N/A",
|
|
302
|
+
Issue: item.warnings.map((w) => w.message).join("; "),
|
|
303
|
+
}));
|
|
304
|
+
|
|
305
|
+
console.table(errorTable);
|
|
306
|
+
|
|
307
|
+
console.log("");
|
|
308
|
+
console.log("%cDetails:", "font-weight: bold;");
|
|
309
|
+
errors.forEach((item, i) => {
|
|
310
|
+
console.log("");
|
|
311
|
+
console.log(`%c${i + 1}. ${item.shortUrl}`, "font-weight: bold;");
|
|
312
|
+
console.log(` URL: ${item.href}`);
|
|
313
|
+
console.log(` Type: ${item.as}`);
|
|
314
|
+
if (item.size > 0) {
|
|
315
|
+
console.log(` Size: ${(item.size / 1024).toFixed(1)} KB`);
|
|
316
|
+
}
|
|
317
|
+
item.warnings.forEach((w) => {
|
|
318
|
+
console.log(` ❌ ${w.message}`);
|
|
319
|
+
});
|
|
320
|
+
console.log(" Element:", item.link);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
console.groupEnd();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (warnings.length > 0) {
|
|
327
|
+
console.log("");
|
|
328
|
+
console.group(`%c⚠️ Warnings (${warnings.length})`, "color: #f59e0b; font-weight: bold;");
|
|
329
|
+
|
|
330
|
+
const warningTable = warnings.map((item) => ({
|
|
331
|
+
Resource: item.shortUrl,
|
|
332
|
+
Type: item.as,
|
|
333
|
+
Size: item.size > 0 ? `${(item.size / 1024).toFixed(1)} KB` : "Unknown",
|
|
334
|
+
Duration: item.duration > 0 ? `${item.duration.toFixed(0)} ms` : "N/A",
|
|
335
|
+
Issue: item.warnings.map((w) => w.message).join("; "),
|
|
336
|
+
}));
|
|
337
|
+
|
|
338
|
+
console.table(warningTable);
|
|
339
|
+
|
|
340
|
+
console.groupEnd();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (infos.length > 0) {
|
|
344
|
+
console.log("");
|
|
345
|
+
console.group(`%cℹ️ Info (${infos.length})`, "color: #3b82f6; font-weight: bold;");
|
|
346
|
+
|
|
347
|
+
const infoTable = infos.map((item) => ({
|
|
348
|
+
Resource: item.shortUrl,
|
|
349
|
+
Type: item.as,
|
|
350
|
+
Status: item.warnings.map((w) => w.message).join("; "),
|
|
351
|
+
}));
|
|
352
|
+
|
|
353
|
+
console.table(infoTable);
|
|
354
|
+
|
|
355
|
+
console.groupEnd();
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Show valid prefetch
|
|
359
|
+
if (validPrefetch.length > 0) {
|
|
360
|
+
console.log("");
|
|
361
|
+
console.group(
|
|
362
|
+
`%c✅ Valid Prefetch (${validPrefetch.length})`,
|
|
363
|
+
"color: #22c55e; font-weight: bold;",
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
const validTable = validPrefetch.map((item) => ({
|
|
367
|
+
Resource: item.shortUrl,
|
|
368
|
+
Type: item.as,
|
|
369
|
+
Size: item.size > 0 ? `${(item.size / 1024).toFixed(1)} KB` : "Unknown",
|
|
370
|
+
Duration: item.duration > 0 ? `${item.duration.toFixed(0)} ms` : "N/A",
|
|
371
|
+
Status: item.loaded ? "✅ Loaded" : "Pending",
|
|
372
|
+
Cache: item.cacheStatus || "N/A",
|
|
373
|
+
}));
|
|
374
|
+
|
|
375
|
+
console.table(validTable);
|
|
376
|
+
|
|
377
|
+
console.groupEnd();
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Best practices
|
|
381
|
+
console.log("");
|
|
382
|
+
console.group("%c📝 Best Practices", "color: #3b82f6; font-weight: bold;");
|
|
383
|
+
console.log("");
|
|
384
|
+
console.log("%c✅ Good uses of prefetch:", "font-weight: bold; color: #22c55e;");
|
|
385
|
+
console.log(" • Next page chunks in predictable navigation (e.g., multi-step forms)");
|
|
386
|
+
console.log(" • Fonts used on commonly visited next pages");
|
|
387
|
+
console.log(" • Small scripts/styles for main navigation targets");
|
|
388
|
+
console.log(" • Resources based on user behavior analytics");
|
|
389
|
+
console.log("");
|
|
390
|
+
console.log("%c❌ Bad uses of prefetch:", "font-weight: bold; color: #ef4444;");
|
|
391
|
+
console.log(" • Resources needed on current page (use preload instead)");
|
|
392
|
+
console.log(" • Large images/videos without user intent analysis");
|
|
393
|
+
console.log(" • Excessive prefetch (>10 resources) hurting mobile users");
|
|
394
|
+
console.log(" • Resources without proper cache headers");
|
|
395
|
+
console.log("");
|
|
396
|
+
console.log("%cRecommended thresholds:", "font-weight: bold;");
|
|
397
|
+
console.log(` • Resource count: <${THRESHOLDS.maxCount} resources`);
|
|
398
|
+
console.log(` • Individual file size: <${THRESHOLDS.largeFileSize / 1024} KB`);
|
|
399
|
+
console.log(` • Total prefetch size: <${THRESHOLDS.totalSizeWarning / (1024 * 1024)} MB`);
|
|
400
|
+
console.log("");
|
|
401
|
+
console.log("%cCode examples:", "font-weight: bold;");
|
|
402
|
+
console.log("");
|
|
403
|
+
console.log("%c ✅ Good: Prefetch next page script", "color: #22c55e;");
|
|
404
|
+
console.log(
|
|
405
|
+
'%c <link rel="prefetch" href="/checkout-page.js" as="script">',
|
|
406
|
+
"font-family: monospace;",
|
|
407
|
+
);
|
|
408
|
+
console.log("");
|
|
409
|
+
console.log("%c ❌ Bad: Prefetch current page resource", "color: #ef4444;");
|
|
410
|
+
console.log(
|
|
411
|
+
'%c <link rel="prefetch" href="/hero-image.jpg" as="image">',
|
|
412
|
+
"font-family: monospace;",
|
|
413
|
+
);
|
|
414
|
+
console.log('%c <!-- Should be: rel="preload" -->', "font-family: monospace; color: #22c55e;");
|
|
415
|
+
|
|
416
|
+
console.groupEnd();
|
|
417
|
+
|
|
418
|
+
// Summary
|
|
419
|
+
if (totalMeaningfulIssues === 0) {
|
|
420
|
+
console.log("");
|
|
421
|
+
console.log(
|
|
422
|
+
"%c✅ Great! No issues detected with prefetch usage.",
|
|
423
|
+
"color: #22c55e; font-weight: bold; font-size: 14px;",
|
|
424
|
+
);
|
|
425
|
+
console.log(
|
|
426
|
+
"%cAll prefetch hints appear appropriate for future navigation.",
|
|
427
|
+
"color: #22c55e;",
|
|
428
|
+
);
|
|
429
|
+
} else {
|
|
430
|
+
console.log("");
|
|
431
|
+
console.log(
|
|
432
|
+
`%c⚠️ Found ${totalMeaningfulIssues} issue(s). Review recommendations above.`,
|
|
433
|
+
"color: #ef4444; font-weight: bold;",
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
console.groupEnd();
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
script: "Prefetch-Resource-Validation",
|
|
441
|
+
status: "ok",
|
|
442
|
+
count: prefetchLinks.length,
|
|
443
|
+
details: {
|
|
444
|
+
loadedCount,
|
|
445
|
+
validCount: validPrefetch.length,
|
|
446
|
+
issueCount: totalMeaningfulIssues,
|
|
447
|
+
totalSizeBytes: totalSize,
|
|
448
|
+
totalTransferBytes: totalTransferSize,
|
|
449
|
+
},
|
|
450
|
+
items: [...validPrefetch, ...issues].map(item => ({ href: item.shortUrl, as: item.as, sizeBytes: item.size, loaded: item.loaded, isCurrentPage: item.isCurrentPage || false })),
|
|
451
|
+
issues: [
|
|
452
|
+
...countIssues.map(i => ({ severity: i.severity === "error" ? "error" : "warning", message: i.message })),
|
|
453
|
+
...issues.flatMap(item => item.warnings.filter(w => w.severity !== "info").map(w => ({ severity: w.severity, message: `${item.shortUrl}: ${w.message}` }))),
|
|
454
|
+
],
|
|
455
|
+
};
|
|
456
|
+
})();
|