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,364 @@
1
+ // SSR Framework Hydration Data Analysis
2
+ // https://webperf-snippets.nucliweb.net
3
+
4
+ (() => {
5
+ const formatBytes = (bytes) => {
6
+ if (bytes === 0) return "0 B";
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(2) + " " + sizes[i];
11
+ };
12
+
13
+ // Framework detection patterns
14
+ const frameworks = [
15
+ {
16
+ name: "Next.js",
17
+ selector: "#__NEXT_DATA__",
18
+ type: "json",
19
+ threshold: 128 * 1024, // 128 KB official warning threshold
20
+ docs: "https://nextjs.org/docs/messages/large-page-data",
21
+ },
22
+ {
23
+ name: "Nuxt 3",
24
+ selector: "#__NUXT_DATA__",
25
+ type: "json-array",
26
+ threshold: 100 * 1024,
27
+ docs: "https://nuxt.com/docs/api/composables/use-hydration",
28
+ },
29
+ {
30
+ name: "Nuxt 2",
31
+ pattern: /window\.__NUXT__\s*=/,
32
+ type: "script",
33
+ threshold: 100 * 1024,
34
+ docs: "https://nuxtjs.org/",
35
+ },
36
+ {
37
+ name: "Remix",
38
+ selector: "#__remixContext",
39
+ type: "script",
40
+ threshold: 100 * 1024,
41
+ docs: "https://remix.run/docs/en/main/guides/performance",
42
+ },
43
+ {
44
+ name: "Gatsby",
45
+ selector: "#___gatsby",
46
+ pattern: /window\.___/,
47
+ type: "script",
48
+ threshold: 100 * 1024,
49
+ docs: "https://www.gatsbyjs.com/docs/",
50
+ },
51
+ {
52
+ name: "SvelteKit",
53
+ selector: "[data-sveltekit-hydrate]",
54
+ type: "json",
55
+ threshold: 100 * 1024,
56
+ docs: "https://kit.svelte.dev/docs/performance",
57
+ },
58
+ {
59
+ name: "Astro",
60
+ selector: "astro-island",
61
+ type: "props",
62
+ threshold: 50 * 1024,
63
+ docs: "https://docs.astro.build/en/concepts/islands/",
64
+ },
65
+ ];
66
+
67
+ // Find all hydration scripts
68
+ const detected = [];
69
+ const allInlineScripts = Array.from(
70
+ document.querySelectorAll("script:not([src])")
71
+ ).filter((s) => s.innerHTML.trim().length > 0);
72
+
73
+ // Check each framework
74
+ frameworks.forEach((fw) => {
75
+ let elements = [];
76
+ let content = "";
77
+ let size = 0;
78
+
79
+ if (fw.selector) {
80
+ const el = document.querySelector(fw.selector);
81
+ if (el) {
82
+ elements = [el];
83
+ content = el.innerHTML || el.textContent || "";
84
+ size = new Blob([content]).size;
85
+ }
86
+ }
87
+
88
+ if (fw.pattern && elements.length === 0) {
89
+ allInlineScripts.forEach((script) => {
90
+ if (fw.pattern.test(script.innerHTML)) {
91
+ elements.push(script);
92
+ content = script.innerHTML;
93
+ size += new Blob([content]).size;
94
+ }
95
+ });
96
+ }
97
+
98
+ // Special handling for Astro islands
99
+ if (fw.name === "Astro") {
100
+ const islands = document.querySelectorAll("astro-island");
101
+ if (islands.length > 0) {
102
+ elements = Array.from(islands);
103
+ let totalProps = 0;
104
+ islands.forEach((island) => {
105
+ const props = island.getAttribute("props");
106
+ if (props) totalProps += new Blob([props]).size;
107
+ });
108
+ size = totalProps;
109
+ content = `${islands.length} islands with props`;
110
+ }
111
+ }
112
+
113
+ if (elements.length > 0) {
114
+ detected.push({
115
+ ...fw,
116
+ elements,
117
+ content,
118
+ size,
119
+ exceedsThreshold: size > fw.threshold,
120
+ });
121
+ }
122
+ });
123
+
124
+ // Calculate other inline scripts
125
+ const frameworkScripts = new Set(detected.flatMap((d) => d.elements));
126
+ const otherScripts = allInlineScripts.filter((s) => !frameworkScripts.has(s));
127
+ const otherSize = otherScripts.reduce(
128
+ (sum, s) => sum + new Blob([s.innerHTML]).size,
129
+ 0
130
+ );
131
+
132
+ // Display results
133
+ console.group(
134
+ "%cšŸš€ SSR Framework Hydration Analysis",
135
+ "font-weight: bold; font-size: 14px;"
136
+ );
137
+
138
+ if (detected.length === 0) {
139
+ console.log("");
140
+ console.log(
141
+ "%cšŸ“­ No SSR framework hydration data detected.",
142
+ "color: #6b7280; font-weight: bold;"
143
+ );
144
+ console.log("This page may be:");
145
+ console.log(" • A static HTML page");
146
+ console.log(" • A client-side rendered SPA");
147
+ console.log(" • Using a framework not yet supported by this snippet");
148
+ console.log("");
149
+ console.log(
150
+ `Found ${otherScripts.length} other inline scripts (${formatBytes(otherSize)})`
151
+ );
152
+ console.groupEnd();
153
+ return { script: "SSR-Hydration-Data-Analysis", status: "ok", count: 0, details: { frameworksFound: 0, totalHydrationBytes: 0, otherInlineBytes: otherSize }, items: [], issues: [] };
154
+ }
155
+
156
+ // Summary
157
+ console.log("");
158
+ console.log("%cDetected Framework(s):", "font-weight: bold;");
159
+ detected.forEach((fw) => {
160
+ const status = fw.exceedsThreshold ? "šŸ”“" : "🟢";
161
+ console.log(
162
+ ` ${status} ${fw.name}: ${formatBytes(fw.size)} (threshold: ${formatBytes(fw.threshold)})`
163
+ );
164
+ });
165
+
166
+ const totalHydrationSize = detected.reduce((sum, d) => sum + d.size, 0);
167
+ console.log("");
168
+ console.log(` Total hydration data: ${formatBytes(totalHydrationSize)}`);
169
+ console.log(` Other inline scripts: ${formatBytes(otherSize)}`);
170
+
171
+ // Detailed analysis for each framework
172
+ detected.forEach((fw) => {
173
+ console.log("");
174
+ console.group(
175
+ `%c${fw.exceedsThreshold ? "šŸ”“" : "🟢"} ${fw.name} Analysis`,
176
+ `color: ${fw.exceedsThreshold ? "#ef4444" : "#22c55e"}; font-weight: bold;`
177
+ );
178
+
179
+ console.log(`Size: ${formatBytes(fw.size)}`);
180
+ console.log(`Threshold: ${formatBytes(fw.threshold)}`);
181
+ console.log(
182
+ `Status: ${fw.exceedsThreshold ? "āš ļø Exceeds recommended limit" : "āœ… Within limits"}`
183
+ );
184
+
185
+ // Parse and analyze content
186
+ if (fw.type === "json" && fw.content) {
187
+ try {
188
+ const data = JSON.parse(fw.content);
189
+ console.log("");
190
+ console.log("%cData Structure:", "font-weight: bold;");
191
+
192
+ if (fw.name === "Next.js" && data.props) {
193
+ const pageProps = data.props?.pageProps || {};
194
+ const pagePropsSize = new Blob([JSON.stringify(pageProps)]).size;
195
+
196
+ console.log(` Build ID: ${data.buildId || "N/A"}`);
197
+ console.log(` Page: ${data.page || "N/A"}`);
198
+ console.log(` pageProps size: ${formatBytes(pagePropsSize)}`);
199
+
200
+ // Analyze pageProps keys
201
+ const propsKeys = Object.keys(pageProps);
202
+ if (propsKeys.length > 0) {
203
+ console.log("");
204
+ console.log("%c pageProps breakdown:", "font-weight: bold;");
205
+
206
+ const propSizes = propsKeys.map((key) => ({
207
+ key,
208
+ size: new Blob([JSON.stringify(pageProps[key])]).size,
209
+ }));
210
+ propSizes.sort((a, b) => b.size - a.size);
211
+
212
+ propSizes.slice(0, 10).forEach((prop) => {
213
+ const pct = ((prop.size / pagePropsSize) * 100).toFixed(1);
214
+ const bar = "ā–ˆ".repeat(Math.min(Math.round(parseFloat(pct) / 5), 20));
215
+ console.log(
216
+ ` ${prop.key}: ${formatBytes(prop.size)} (${pct}%) ${bar}`
217
+ );
218
+ });
219
+
220
+ if (propsKeys.length > 10) {
221
+ console.log(` ... and ${propsKeys.length - 10} more props`);
222
+ }
223
+ }
224
+
225
+ // Check for common issues
226
+ console.log("");
227
+ console.log("%cPotential Issues:", "font-weight: bold;");
228
+
229
+ // Large arrays
230
+ const largeArrays = propsKeys.filter((key) => {
231
+ const val = pageProps[key];
232
+ return Array.isArray(val) && val.length > 50;
233
+ });
234
+ if (largeArrays.length > 0) {
235
+ console.log(
236
+ ` āš ļø Large arrays: ${largeArrays.join(", ")} (consider pagination)`
237
+ );
238
+ }
239
+
240
+ // Deeply nested objects
241
+ const checkDepth = (obj, depth = 0) => {
242
+ if (depth > 5) return true;
243
+ if (typeof obj !== "object" || obj === null) return false;
244
+ return Object.values(obj).some((v) => checkDepth(v, depth + 1));
245
+ };
246
+ if (checkDepth(pageProps)) {
247
+ console.log(" āš ļø Deeply nested data (> 5 levels)");
248
+ }
249
+
250
+ // Potential sensitive data patterns
251
+ const sensitivePatterns = /password|secret|token|apikey|api_key|private/i;
252
+ const propsString = JSON.stringify(pageProps);
253
+ if (sensitivePatterns.test(propsString)) {
254
+ console.log(
255
+ " 🚨 Possible sensitive data detected - review prop names"
256
+ );
257
+ }
258
+
259
+ if (
260
+ largeArrays.length === 0 &&
261
+ !checkDepth(pageProps) &&
262
+ !sensitivePatterns.test(propsString)
263
+ ) {
264
+ console.log(" āœ… No obvious issues detected");
265
+ }
266
+ }
267
+
268
+ // Raw data reference
269
+ console.log("");
270
+ console.log("%cšŸ“¦ Raw data object:", "font-weight: bold;");
271
+ console.log(data);
272
+ } catch {
273
+ console.log("Could not parse JSON content");
274
+ }
275
+ }
276
+
277
+ if (fw.name === "Astro") {
278
+ console.log("");
279
+ console.log(`Islands found: ${fw.elements.length}`);
280
+ const islandTable = fw.elements.map((island, i) => {
281
+ const props = island.getAttribute("props");
282
+ const clientDirective =
283
+ island.getAttribute("client") ||
284
+ Array.from(island.attributes)
285
+ .find((a) => a.name.startsWith("client:"))
286
+ ?.name.replace("client:", "") ||
287
+ "unknown";
288
+ return {
289
+ "#": i + 1,
290
+ Component: island.getAttribute("component-url")?.split("/").pop() || "Unknown",
291
+ Client: clientDirective,
292
+ "Props Size": props ? formatBytes(new Blob([props]).size) : "0 B",
293
+ };
294
+ });
295
+ console.table(islandTable);
296
+ }
297
+
298
+ // Element reference
299
+ console.log("");
300
+ console.log("%cšŸ”Ž Element(s):", "font-weight: bold;");
301
+ fw.elements.forEach((el, i) => {
302
+ console.log(`${i + 1}.`, el);
303
+ });
304
+
305
+ console.log("");
306
+ console.log(`šŸ“š Docs: ${fw.docs}`);
307
+
308
+ console.groupEnd();
309
+ });
310
+
311
+ // Recommendations
312
+ const hasIssues = detected.some((d) => d.exceedsThreshold);
313
+
314
+ if (hasIssues) {
315
+ console.log("");
316
+ console.group("%cšŸ“ Recommendations", "color: #3b82f6; font-weight: bold;");
317
+ console.log("");
318
+ console.log("%cTo reduce hydration data size:", "font-weight: bold;");
319
+ console.log("");
320
+ console.log("1. Only fetch data you actually render");
321
+ console.log(" → Remove unused fields from API responses");
322
+ console.log(" → Use GraphQL or tRPC for precise data fetching");
323
+ console.log("");
324
+ console.log("2. Paginate large lists");
325
+ console.log(" → Don't send 100+ items in initial props");
326
+ console.log(" → Implement infinite scroll or pagination");
327
+ console.log("");
328
+ console.log("3. Defer non-critical data");
329
+ console.log(" → Fetch some data client-side after hydration");
330
+ console.log(" → Use React Query, SWR, or similar");
331
+ console.log("");
332
+ console.log("4. Transform data before sending");
333
+ console.log(" → Remove unnecessary nested data");
334
+ console.log(" → Flatten structures where possible");
335
+ console.log("");
336
+
337
+ const nextJs = detected.find((d) => d.name === "Next.js");
338
+ if (nextJs) {
339
+ console.log("%cNext.js specific:", "font-weight: bold;");
340
+ console.log(
341
+ '%c// In getServerSideProps or getStaticProps:\nreturn {\n props: {\n // Only include what the page renders\n items: items.slice(0, 10), // Paginate\n // Omit unused fields\n user: { name: user.name, avatar: user.avatar },\n },\n};',
342
+ "font-family: monospace; color: #22c55e;"
343
+ );
344
+ }
345
+
346
+ console.groupEnd();
347
+ }
348
+
349
+ console.groupEnd();
350
+
351
+ return {
352
+ script: "SSR-Hydration-Data-Analysis",
353
+ status: "ok",
354
+ count: detected.length,
355
+ details: {
356
+ frameworksFound: detected.length,
357
+ totalHydrationBytes: totalHydrationSize,
358
+ otherInlineBytes: otherSize,
359
+ hasExceedingThreshold: detected.some(d => d.exceedsThreshold),
360
+ },
361
+ items: detected.map(fw => ({ name: fw.name, sizeBytes: fw.size, thresholdBytes: fw.threshold, exceedsThreshold: fw.exceedsThreshold })),
362
+ issues: detected.filter(fw => fw.exceedsThreshold).map(fw => ({ severity: "warning", message: `${fw.name} hydration data (${Math.round(fw.size / 1024)} KB) exceeds ${Math.round(fw.threshold / 1024)} KB threshold` })),
363
+ };
364
+ })();
@@ -0,0 +1,332 @@
1
+ // Script Loading 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
+ const formatMs = (ms) => (ms > 0 ? ms.toFixed(0) + "ms" : "-");
14
+
15
+ // Get current page's root domain for first/third party detection
16
+ const getRootDomain = (hostname) => {
17
+ const parts = hostname.split(".");
18
+ if (parts.length > 2) {
19
+ const sld = parts[parts.length - 2];
20
+ if (["co", "com", "org", "net", "gov", "edu"].includes(sld) && sld.length <= 3) {
21
+ return parts.slice(-3).join(".");
22
+ }
23
+ return parts.slice(-2).join(".");
24
+ }
25
+ return hostname;
26
+ };
27
+
28
+ const currentDomain = getRootDomain(location.hostname);
29
+ const isFirstParty = (url) => {
30
+ try {
31
+ return getRootDomain(new URL(url).hostname) === currentDomain;
32
+ } catch {
33
+ return true;
34
+ }
35
+ };
36
+
37
+ // Get resource timing data
38
+ const resourceTiming = new Map();
39
+ performance.getEntriesByType("resource").forEach((r) => {
40
+ if (r.initiatorType === "script") {
41
+ resourceTiming.set(r.name, {
42
+ size: r.transferSize || 0,
43
+ duration: r.duration,
44
+ startTime: r.startTime,
45
+ });
46
+ }
47
+ });
48
+
49
+ // Analyze all external scripts
50
+ const externalScripts = Array.from(document.querySelectorAll("script[src]"));
51
+
52
+ // Analyze inline scripts
53
+ const inlineScripts = Array.from(document.querySelectorAll("script:not([src])"))
54
+ .filter((s) => s.innerHTML.trim().length > 0);
55
+
56
+ const scripts = externalScripts.map((script) => {
57
+ const src = script.src;
58
+ const timing = resourceTiming.get(src) || {};
59
+ const inHead = script.closest("head") !== null;
60
+ const isModule = script.type === "module";
61
+ const isAsync = script.async;
62
+ const isDefer = script.defer;
63
+
64
+ // Determine loading strategy
65
+ let strategy = "blocking";
66
+ if (isModule && isAsync) {
67
+ strategy = "async module";
68
+ } else if (isModule) {
69
+ strategy = "module";
70
+ } else if (isAsync) {
71
+ strategy = "async";
72
+ } else if (isDefer) {
73
+ strategy = "defer";
74
+ }
75
+
76
+ const isBlocking = strategy === "blocking";
77
+ const firstParty = isFirstParty(src);
78
+
79
+ return {
80
+ src,
81
+ shortSrc: src.split("/").pop()?.split("?")[0] || src,
82
+ strategy,
83
+ isBlocking,
84
+ inHead,
85
+ firstParty,
86
+ isModule,
87
+ isAsync,
88
+ isDefer,
89
+ size: timing.size || 0,
90
+ duration: timing.duration || 0,
91
+ startTime: timing.startTime || 0,
92
+ element: script,
93
+ };
94
+ });
95
+
96
+ // Categorize scripts
97
+ const blocking = scripts.filter((s) => s.isBlocking);
98
+ const blockingInHead = blocking.filter((s) => s.inHead);
99
+ const asyncScripts = scripts.filter((s) => s.strategy === "async" || s.strategy === "async module");
100
+ const deferScripts = scripts.filter((s) => s.strategy === "defer");
101
+ const moduleScripts = scripts.filter((s) => s.strategy === "module");
102
+ const thirdPartyBlocking = blocking.filter((s) => !s.firstParty);
103
+
104
+ // Calculate totals
105
+ const totalSize = scripts.reduce((sum, s) => sum + s.size, 0);
106
+ const blockingSize = blocking.reduce((sum, s) => sum + s.size, 0);
107
+
108
+ // Rating
109
+ let rating, ratingColor;
110
+ if (blockingInHead.length === 0) {
111
+ rating = "Good";
112
+ ratingColor = "#22c55e";
113
+ } else if (blockingInHead.length <= 2 && thirdPartyBlocking.length === 0) {
114
+ rating = "Needs Review";
115
+ ratingColor = "#f59e0b";
116
+ } else {
117
+ rating = "Needs Optimization";
118
+ ratingColor = "#ef4444";
119
+ }
120
+
121
+ // Display results
122
+ console.group("%cšŸ“œ Script Loading Analysis", "font-weight: bold; font-size: 14px;");
123
+
124
+ // Summary
125
+ console.log("");
126
+ console.log("%cSummary:", "font-weight: bold;");
127
+ console.log(` Total external scripts: ${scripts.length}`);
128
+ console.log(` Total inline scripts: ${inlineScripts.length}`);
129
+ console.log(` Total size: ${formatBytes(totalSize)}`);
130
+ console.log("");
131
+ console.log("%cBy loading strategy:", "font-weight: bold;");
132
+ console.log(` šŸ”“ Blocking: ${blocking.length} (${formatBytes(blockingSize)})`);
133
+ console.log(` ⚔ Async: ${asyncScripts.length}`);
134
+ console.log(` šŸ“‹ Defer: ${deferScripts.length}`);
135
+ console.log(` šŸ“¦ Module: ${moduleScripts.length}`);
136
+ console.log("");
137
+ console.log(` Rating: %c${rating}`, `color: ${ratingColor}; font-weight: bold;`);
138
+
139
+ // Detailed table
140
+ if (scripts.length > 0) {
141
+ console.log("");
142
+ console.group("%cšŸ“Š External Scripts", "color: #3b82f6; font-weight: bold;");
143
+
144
+ const tableData = scripts
145
+ .sort((a, b) => {
146
+ // Sort: blocking first, then by size
147
+ if (a.isBlocking !== b.isBlocking) return a.isBlocking ? -1 : 1;
148
+ return b.size - a.size;
149
+ })
150
+ .map((s) => ({
151
+ Script: s.shortSrc,
152
+ Strategy: s.isBlocking ? `šŸ”“ ${s.strategy}` : s.strategy,
153
+ Location: s.inHead ? "head" : "body",
154
+ Party: s.firstParty ? "1st" : "3rd",
155
+ Size: formatBytes(s.size),
156
+ Duration: formatMs(s.duration),
157
+ }));
158
+
159
+ console.table(tableData);
160
+
161
+ // Elements for inspection
162
+ console.log("");
163
+ console.log("%cšŸ”Ž Elements (sorted by load order):", "font-weight: bold;");
164
+ scripts
165
+ .sort((a, b) => a.startTime - b.startTime)
166
+ .forEach((s, i) => {
167
+ const marker = s.isBlocking ? "šŸ”“" : "āœ…";
168
+ console.log(`${i + 1}. ${marker} ${s.shortSrc}`, s.element);
169
+ });
170
+
171
+ console.groupEnd();
172
+ }
173
+
174
+ // Issues
175
+ const issues = [];
176
+
177
+ if (blockingInHead.length > 0) {
178
+ issues.push({
179
+ severity: "error",
180
+ message: `${blockingInHead.length} blocking script(s) in <head>`,
181
+ scripts: blockingInHead,
182
+ fix: "Add 'defer' or 'async' attribute, or move to end of <body>",
183
+ });
184
+ }
185
+
186
+ if (thirdPartyBlocking.length > 0) {
187
+ issues.push({
188
+ severity: "error",
189
+ message: `${thirdPartyBlocking.length} third-party blocking script(s)`,
190
+ scripts: thirdPartyBlocking,
191
+ fix: "Add 'async' for independent scripts, or load dynamically",
192
+ });
193
+ }
194
+
195
+ const largeBlocking = blocking.filter((s) => s.size > 50 * 1024);
196
+ if (largeBlocking.length > 0) {
197
+ issues.push({
198
+ severity: "warning",
199
+ message: `${largeBlocking.length} large blocking script(s) (> 50 KB)`,
200
+ scripts: largeBlocking,
201
+ fix: "Split code, use defer, or lazy load",
202
+ });
203
+ }
204
+
205
+ // Scripts that could be deferred
206
+ const couldDefer = blocking.filter((s) => {
207
+ // Scripts at end of body could likely use defer
208
+ const bodyScripts = Array.from(document.body.querySelectorAll("script[src]"));
209
+ const isLastInBody = bodyScripts.indexOf(s.element) >= bodyScripts.length - 3;
210
+ return isLastInBody;
211
+ });
212
+
213
+ if (couldDefer.length > 0 && blocking.length > couldDefer.length) {
214
+ issues.push({
215
+ severity: "info",
216
+ message: `${blocking.length - couldDefer.length} blocking script(s) could potentially use defer`,
217
+ fix: "Test with defer attribute to improve parsing performance",
218
+ });
219
+ }
220
+
221
+ if (issues.length > 0) {
222
+ console.log("");
223
+ console.group("%cāš ļø Issues Found", "color: #ef4444; font-weight: bold;");
224
+
225
+ issues.forEach((issue) => {
226
+ const icon = issue.severity === "error" ? "šŸ”“" : issue.severity === "warning" ? "🟔" : "šŸ’”";
227
+ console.log("");
228
+ console.log(`%c${icon} ${issue.message}`, "font-weight: bold;");
229
+ if (issue.scripts) {
230
+ issue.scripts.forEach((s) => {
231
+ console.log(` • ${s.shortSrc} (${formatBytes(s.size)})`);
232
+ });
233
+ }
234
+ console.log(` → ${issue.fix}`);
235
+ });
236
+
237
+ console.groupEnd();
238
+ } else if (scripts.length > 0) {
239
+ console.log("");
240
+ console.log(
241
+ "%cāœ… All scripts use non-blocking loading strategies!",
242
+ "color: #22c55e; font-weight: bold;"
243
+ );
244
+ }
245
+
246
+ // First/Third party breakdown
247
+ const firstPartyScripts = scripts.filter((s) => s.firstParty);
248
+ const thirdPartyScripts = scripts.filter((s) => !s.firstParty);
249
+
250
+ if (thirdPartyScripts.length > 0) {
251
+ console.log("");
252
+ console.group("%c🌐 Third-Party Scripts", "color: #8b5cf6; font-weight: bold;");
253
+
254
+ const thirdPartyByHost = new Map();
255
+ thirdPartyScripts.forEach((s) => {
256
+ try {
257
+ const host = new URL(s.src).hostname;
258
+ if (!thirdPartyByHost.has(host)) {
259
+ thirdPartyByHost.set(host, { count: 0, size: 0, blocking: 0 });
260
+ }
261
+ const data = thirdPartyByHost.get(host);
262
+ data.count++;
263
+ data.size += s.size;
264
+ if (s.isBlocking) data.blocking++;
265
+ } catch {}
266
+ });
267
+
268
+ const hostTable = Array.from(thirdPartyByHost.entries())
269
+ .sort((a, b) => b[1].size - a[1].size)
270
+ .map(([host, data]) => ({
271
+ Host: host,
272
+ Scripts: data.count,
273
+ Size: formatBytes(data.size),
274
+ Blocking: data.blocking > 0 ? `āš ļø ${data.blocking}` : "0",
275
+ }));
276
+
277
+ console.table(hostTable);
278
+ console.groupEnd();
279
+ }
280
+
281
+ // Best practices (defined here so firstPartyScripts/thirdPartyScripts are in scope for return)
282
+ console.log("");
283
+ console.group("%cšŸ“ Best Practices", "color: #3b82f6; font-weight: bold;");
284
+ console.log("");
285
+ console.log("%cFor most scripts (need DOM, run in order):", "font-weight: bold;");
286
+ console.log('%c<script src="app.js" defer></script>', "font-family: monospace; color: #22c55e;");
287
+ console.log("");
288
+ console.log("%cFor independent scripts (analytics, ads):", "font-weight: bold;");
289
+ console.log('%c<script src="analytics.js" async></script>', "font-family: monospace; color: #22c55e;");
290
+ console.log("");
291
+ console.log("%cFor ES modules:", "font-weight: bold;");
292
+ console.log('%c<script type="module" src="app.mjs"></script>', "font-family: monospace; color: #22c55e;");
293
+ console.log("");
294
+ console.log("%cFor critical inline scripts:", "font-weight: bold;");
295
+ console.log('%c<script>/* Only essential initialization */</script>', "font-family: monospace; color: #22c55e;");
296
+ console.groupEnd();
297
+
298
+ console.groupEnd();
299
+
300
+ const agentRating = blockingInHead.length === 0 ? "good" :
301
+ (blockingInHead.length <= 2 && thirdPartyBlocking.length === 0) ? "needs-improvement" : "poor";
302
+ return {
303
+ script: "Script-Loading",
304
+ status: "ok",
305
+ count: scripts.length,
306
+ rating: agentRating,
307
+ details: {
308
+ totalSizeBytes: totalSize,
309
+ byStrategy: {
310
+ blocking: blocking.length,
311
+ async: asyncScripts.length,
312
+ defer: deferScripts.length,
313
+ module: moduleScripts.length,
314
+ },
315
+ byParty: {
316
+ firstParty: firstPartyScripts.length,
317
+ thirdParty: thirdPartyScripts.length,
318
+ },
319
+ thirdPartyBlockingCount: thirdPartyBlocking.length,
320
+ },
321
+ items: scripts.map((s) => ({
322
+ url: s.src,
323
+ shortName: s.shortSrc,
324
+ strategy: s.strategy,
325
+ location: s.inHead ? "head" : "body",
326
+ party: s.firstParty ? "first" : "third",
327
+ sizeBytes: s.size,
328
+ durationMs: Math.round(s.duration),
329
+ })),
330
+ issues: issues.map((i) => ({ severity: i.severity, message: i.message })),
331
+ };
332
+ })();