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,269 @@
1
+ // First and Third Party Script Analysis
2
+ // https://webperf-snippets.nucliweb.net
3
+
4
+ (() => {
5
+ // Auto-detect first-party: same root domain (handles subdomains)
6
+ function getRootDomain(hostname) {
7
+ const parts = hostname.split(".");
8
+ // Handle cases like co.uk, com.au, etc.
9
+ if (parts.length > 2) {
10
+ // Simple heuristic: if second-to-last part is short (co, com, org, net), take 3 parts
11
+ const sld = parts[parts.length - 2];
12
+ if (sld.length <= 3 && ["co", "com", "org", "net", "gov", "edu"].includes(sld)) {
13
+ return parts.slice(-3).join(".");
14
+ }
15
+ return parts.slice(-2).join(".");
16
+ }
17
+ return hostname;
18
+ }
19
+
20
+ const currentRootDomain = getRootDomain(location.hostname);
21
+
22
+ function isFirstParty(hostname) {
23
+ return getRootDomain(hostname) === currentRootDomain;
24
+ }
25
+
26
+ function formatBytes(bytes) {
27
+ if (!bytes || bytes === 0) return "N/A";
28
+ const k = 1024;
29
+ const sizes = ["B", "KB", "MB"];
30
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
31
+ return (bytes / Math.pow(k, i)).toFixed(1) + " " + sizes[i];
32
+ }
33
+
34
+ // Gather script data
35
+ const scripts = performance
36
+ .getEntriesByType("resource")
37
+ .filter((r) => r.initiatorType === "script")
38
+ .map((r) => {
39
+ const url = new URL(r.name);
40
+ const firstParty = isFirstParty(url.hostname);
41
+
42
+ return {
43
+ name: r.name,
44
+ shortName: url.pathname.split("/").pop() || url.pathname,
45
+ host: url.hostname,
46
+ firstParty,
47
+ duration: r.duration,
48
+ transferSize: r.transferSize || 0,
49
+ startTime: r.startTime,
50
+ responseEnd: r.responseEnd,
51
+ renderBlocking: r.renderBlockingStatus === "blocking",
52
+ };
53
+ });
54
+
55
+ const firstParty = scripts.filter((s) => s.firstParty);
56
+ const thirdParty = scripts.filter((s) => !s.firstParty);
57
+
58
+ // Calculate metrics
59
+ const calcMetrics = (list) => ({
60
+ count: list.length,
61
+ totalSize: list.reduce((sum, s) => sum + s.transferSize, 0),
62
+ totalDuration: list.reduce((sum, s) => sum + s.duration, 0),
63
+ blocking: list.filter((s) => s.renderBlocking).length,
64
+ hosts: [...new Set(list.map((s) => s.host))],
65
+ });
66
+
67
+ const firstMetrics = calcMetrics(firstParty);
68
+ const thirdMetrics = calcMetrics(thirdParty);
69
+ const totalScripts = scripts.length;
70
+ const thirdPartyPct =
71
+ totalScripts > 0 ? ((thirdParty.length / totalScripts) * 100).toFixed(0) : 0;
72
+
73
+ // Display results
74
+ console.group(
75
+ "%cšŸ“Š Script Analysis: First vs Third Party",
76
+ "font-weight: bold; font-size: 14px;",
77
+ );
78
+
79
+ // Overall summary
80
+ console.log("");
81
+ console.log("%cOverall Summary:", "font-weight: bold;");
82
+ console.log(` Total scripts: ${totalScripts}`);
83
+ console.log(` First-party: ${firstParty.length} (${100 - thirdPartyPct}%)`);
84
+ console.log(` Third-party: ${thirdParty.length} (${thirdPartyPct}%)`);
85
+
86
+ if (thirdParty.length > firstParty.length) {
87
+ console.log(`%c āš ļø More third-party scripts than first-party!`, "color: #f59e0b;");
88
+ }
89
+
90
+ // First Party Section
91
+ console.log("");
92
+ console.group(
93
+ `%cšŸ  First-Party Scripts (${firstParty.length})`,
94
+ "color: #22c55e; font-weight: bold;",
95
+ );
96
+
97
+ if (firstParty.length === 0) {
98
+ console.log("No first-party scripts found.");
99
+ } else {
100
+ console.log(` Total size: ${formatBytes(firstMetrics.totalSize)}`);
101
+ console.log(` Render-blocking: ${firstMetrics.blocking}`);
102
+ console.log("");
103
+
104
+ const firstTable = firstParty
105
+ .sort((a, b) => b.transferSize - a.transferSize)
106
+ .map((s) => ({
107
+ Script: s.shortName,
108
+ Size: formatBytes(s.transferSize),
109
+ Duration: s.duration.toFixed(0) + "ms",
110
+ Blocking: s.renderBlocking ? "āš ļø Yes" : "No",
111
+ Host: s.host,
112
+ }));
113
+ console.table(firstTable);
114
+ }
115
+ console.groupEnd();
116
+
117
+ // Third Party Section
118
+ console.log("");
119
+ console.group(
120
+ `%c🌐 Third-Party Scripts (${thirdParty.length})`,
121
+ "color: #ef4444; font-weight: bold;",
122
+ );
123
+
124
+ if (thirdParty.length === 0) {
125
+ console.log("%cāœ… No third-party scripts found!", "color: #22c55e; font-weight: bold;");
126
+ } else {
127
+ console.log(` Total size: ${formatBytes(thirdMetrics.totalSize)}`);
128
+ console.log(` Render-blocking: ${thirdMetrics.blocking}`);
129
+ console.log(` Unique hosts: ${thirdMetrics.hosts.length}`);
130
+ console.log("");
131
+
132
+ // Group by host
133
+ console.log("%c By host:", "font-weight: bold;");
134
+ const byHost = thirdParty.reduce((acc, s) => {
135
+ if (!acc[s.host]) acc[s.host] = { count: 0, size: 0, blocking: 0 };
136
+ acc[s.host].count++;
137
+ acc[s.host].size += s.transferSize;
138
+ if (s.renderBlocking) acc[s.host].blocking++;
139
+ return acc;
140
+ }, {});
141
+
142
+ Object.entries(byHost)
143
+ .sort((a, b) => b[1].size - a[1].size)
144
+ .forEach(([host, data]) => {
145
+ const blockingMark = data.blocking > 0 ? " āš ļø" : "";
146
+ console.log(
147
+ ` ${host}: ${data.count} script(s), ${formatBytes(data.size)}${blockingMark}`,
148
+ );
149
+ });
150
+
151
+ console.log("");
152
+ const thirdTable = thirdParty
153
+ .sort((a, b) => b.transferSize - a.transferSize)
154
+ .map((s) => ({
155
+ Script: s.shortName,
156
+ Host: s.host,
157
+ Size: formatBytes(s.transferSize),
158
+ Duration: s.duration.toFixed(0) + "ms",
159
+ Blocking: s.renderBlocking ? "āš ļø Yes" : "No",
160
+ }));
161
+ console.table(thirdTable);
162
+ }
163
+ console.groupEnd();
164
+
165
+ // Recommendations
166
+ if (thirdParty.length > 0) {
167
+ console.log("");
168
+ console.group("%cšŸ“ Recommendations", "color: #3b82f6; font-weight: bold;");
169
+
170
+ if (thirdMetrics.blocking > 0) {
171
+ console.log("");
172
+ console.log(
173
+ "%cāš ļø Render-blocking third-party scripts:",
174
+ "font-weight: bold; color: #ef4444;",
175
+ );
176
+ console.log(" • Load with 'async' or 'defer' attribute");
177
+ console.log(" • Consider lazy-loading after user interaction");
178
+ console.log(
179
+ '%c <script src="..." async></script>',
180
+ "font-family: monospace; color: #22c55e;",
181
+ );
182
+ }
183
+
184
+ if (thirdMetrics.hosts.length > 3) {
185
+ console.log("");
186
+ console.log(
187
+ `%cāš ļø ${thirdMetrics.hosts.length} different third-party hosts:`,
188
+ "font-weight: bold; color: #f59e0b;",
189
+ );
190
+ console.log(" • Each host requires DNS lookup + connection");
191
+ console.log(" • Use <link rel='preconnect'> for critical hosts");
192
+ console.log(
193
+ '%c <link rel="preconnect" href="https://web.dev">',
194
+ "font-family: monospace; color: #22c55e;",
195
+ );
196
+ }
197
+
198
+ if (thirdMetrics.totalSize > 100 * 1024) {
199
+ console.log("");
200
+ console.log(
201
+ `%cāš ļø Third-party scripts total ${formatBytes(thirdMetrics.totalSize)}:`,
202
+ "font-weight: bold; color: #f59e0b;",
203
+ );
204
+ console.log(" • Audit necessity of each script");
205
+ console.log(" • Consider self-hosting critical scripts");
206
+ console.log(" • Look for lighter alternatives");
207
+ }
208
+
209
+ console.log("");
210
+ console.log("%cšŸ’” General tips:", "font-weight: bold;");
211
+ console.log(" • Regularly audit third-party scripts");
212
+ console.log(" • Set up Content Security Policy (CSP)");
213
+ console.log(" • Monitor third-party performance with RUM");
214
+ console.log(" • Have fallbacks for critical functionality");
215
+
216
+ console.groupEnd();
217
+ }
218
+
219
+ console.groupEnd();
220
+
221
+ return {
222
+ script: "First-And-Third-Party-Script-Info",
223
+ status: "ok",
224
+ count: totalScripts,
225
+ details: {
226
+ firstPartyCount: firstParty.length,
227
+ thirdPartyCount: thirdParty.length,
228
+ thirdPartyPercent: Number(thirdPartyPct),
229
+ firstPartySizeBytes: firstMetrics.totalSize,
230
+ thirdPartySizeBytes: thirdMetrics.totalSize,
231
+ thirdPartyBlockingCount: thirdMetrics.blocking,
232
+ thirdPartyHostCount: thirdMetrics.hosts.length,
233
+ },
234
+ items: scripts.map((s) => ({
235
+ shortName: s.shortName,
236
+ host: s.host,
237
+ firstParty: s.firstParty,
238
+ transferBytes: s.transferSize,
239
+ durationMs: Math.round(s.duration),
240
+ renderBlocking: s.renderBlocking,
241
+ })),
242
+ issues: [
243
+ ...(thirdMetrics.blocking > 0
244
+ ? [
245
+ {
246
+ severity: "error",
247
+ message: `${thirdMetrics.blocking} render-blocking third-party script(s)`,
248
+ },
249
+ ]
250
+ : []),
251
+ ...(thirdMetrics.hosts.length > 3
252
+ ? [
253
+ {
254
+ severity: "warning",
255
+ message: `${thirdMetrics.hosts.length} different third-party hosts require separate DNS lookups`,
256
+ },
257
+ ]
258
+ : []),
259
+ ...(thirdMetrics.totalSize > 100 * 1024
260
+ ? [
261
+ {
262
+ severity: "warning",
263
+ message: `Third-party scripts total ${Math.round(thirdMetrics.totalSize / 1024)} KB`,
264
+ },
265
+ ]
266
+ : []),
267
+ ],
268
+ };
269
+ })();
@@ -0,0 +1,295 @@
1
+ // First and Third Party Script Timings Analysis
2
+ // https://webperf-snippets.nucliweb.net
3
+
4
+ (() => {
5
+ // Auto-detect first-party by root domain
6
+ function getRootDomain(hostname) {
7
+ const parts = hostname.split(".");
8
+ if (parts.length > 2) {
9
+ const sld = parts[parts.length - 2];
10
+ if (sld.length <= 3 && ["co", "com", "org", "net", "gov", "edu"].includes(sld)) {
11
+ return parts.slice(-3).join(".");
12
+ }
13
+ return parts.slice(-2).join(".");
14
+ }
15
+ return hostname;
16
+ }
17
+
18
+ const currentRootDomain = getRootDomain(location.hostname);
19
+
20
+ function isFirstParty(hostname) {
21
+ return getRootDomain(hostname) === currentRootDomain;
22
+ }
23
+
24
+ // Gather script timing data
25
+ const scripts = performance
26
+ .getEntriesByType("resource")
27
+ .filter((r) => r.initiatorType === "script")
28
+ .map((r) => {
29
+ const url = new URL(r.name);
30
+ const firstParty = isFirstParty(url.hostname);
31
+
32
+ // Check if timing data is available (CORS)
33
+ const hasTiming = r.requestStart > 0;
34
+
35
+ // Calculate timing phases
36
+ const timings = {
37
+ dns: r.domainLookupEnd - r.domainLookupStart,
38
+ tcp: r.connectEnd - r.connectStart,
39
+ tls: r.secureConnectionStart > 0 ? r.connectEnd - r.secureConnectionStart : 0,
40
+ request: r.responseStart - r.requestStart,
41
+ response: r.responseEnd - r.responseStart,
42
+ total: r.responseEnd - r.startTime,
43
+ };
44
+
45
+ // Adjust TCP to exclude TLS time
46
+ if (r.secureConnectionStart > 0) {
47
+ timings.tcp = r.secureConnectionStart - r.connectStart;
48
+ }
49
+
50
+ return {
51
+ name: r.name,
52
+ shortName: url.pathname.split("/").pop() || url.hostname,
53
+ host: url.hostname,
54
+ firstParty,
55
+ hasTiming,
56
+ size: r.transferSize || 0,
57
+ ...timings,
58
+ };
59
+ });
60
+
61
+ const firstParty = scripts.filter((s) => s.firstParty);
62
+ const thirdParty = scripts.filter((s) => !s.firstParty);
63
+
64
+ // Calculate aggregate metrics
65
+ function calcStats(list) {
66
+ const withTiming = list.filter((s) => s.hasTiming);
67
+ const phases = ["dns", "tcp", "tls", "request", "response", "total"];
68
+
69
+ const stats = {};
70
+ phases.forEach((phase) => {
71
+ const values = withTiming.map((s) => s[phase]).filter((v) => v > 0);
72
+ if (values.length > 0) {
73
+ stats[phase] = {
74
+ avg: values.reduce((a, b) => a + b, 0) / values.length,
75
+ max: Math.max(...values),
76
+ total: values.reduce((a, b) => a + b, 0),
77
+ };
78
+ } else {
79
+ stats[phase] = { avg: 0, max: 0, total: 0 };
80
+ }
81
+ });
82
+
83
+ return {
84
+ count: list.length,
85
+ withTiming: withTiming.length,
86
+ withoutTiming: list.length - withTiming.length,
87
+ stats,
88
+ };
89
+ }
90
+
91
+ const firstStats = calcStats(firstParty);
92
+ const thirdStats = calcStats(thirdParty);
93
+
94
+ // Format helpers
95
+ const formatMs = (ms) => (ms > 0 ? ms.toFixed(1) + "ms" : "-");
96
+ const formatBar = (value, max) => {
97
+ if (value <= 0 || max <= 0) return "";
98
+ const width = Math.min(Math.round((value / max) * 15), 15);
99
+ return "ā–ˆ".repeat(width) + "ā–‘".repeat(15 - width);
100
+ };
101
+
102
+ // Display results
103
+ console.group("%cā±ļø Script Timing Analysis", "font-weight: bold; font-size: 14px;");
104
+
105
+ // Summary comparison
106
+ console.log("");
107
+ console.log("%cSummary (averages):", "font-weight: bold;");
108
+
109
+ const phases = [
110
+ { key: "dns", name: "DNS Lookup", icon: "šŸ”" },
111
+ { key: "tcp", name: "TCP Connection", icon: "šŸ”Œ" },
112
+ { key: "tls", name: "TLS/SSL", icon: "šŸ”’" },
113
+ { key: "request", name: "Request (TTFB)", icon: "šŸ“¤" },
114
+ { key: "response", name: "Response", icon: "šŸ“„" },
115
+ { key: "total", name: "Total", icon: "ā±ļø" },
116
+ ];
117
+
118
+ console.log("");
119
+ console.log(" First-Party Third-Party");
120
+ console.log(" ─────────── ───────────");
121
+
122
+ phases.forEach(({ key, name, icon }) => {
123
+ const fp = formatMs(firstStats.stats[key]?.avg || 0).padStart(10);
124
+ const tp = formatMs(thirdStats.stats[key]?.avg || 0).padStart(10);
125
+ console.log(`${icon} ${name.padEnd(18)} ${fp} ${tp}`);
126
+ });
127
+
128
+ // First-party details
129
+ console.log("");
130
+ console.group(`%cšŸ  First-Party Scripts (${firstParty.length})`, "color: #22c55e; font-weight: bold;");
131
+
132
+ if (firstParty.length === 0) {
133
+ console.log("No first-party scripts found.");
134
+ } else {
135
+ if (firstStats.withoutTiming > 0) {
136
+ console.log(`%cāš ļø ${firstStats.withoutTiming} script(s) have no detailed timing (cached or same-origin)`, "color: #f59e0b;");
137
+ }
138
+
139
+ const maxTotal = Math.max(...firstParty.map((s) => s.total));
140
+
141
+ const tableData = firstParty
142
+ .sort((a, b) => b.total - a.total)
143
+ .slice(0, 15)
144
+ .map((s) => ({
145
+ Script: s.shortName,
146
+ DNS: formatMs(s.dns),
147
+ TCP: formatMs(s.tcp),
148
+ TLS: formatMs(s.tls),
149
+ Request: formatMs(s.request),
150
+ Response: formatMs(s.response),
151
+ Total: formatMs(s.total),
152
+ "": formatBar(s.total, maxTotal),
153
+ }));
154
+
155
+ console.table(tableData);
156
+
157
+ if (firstParty.length > 15) {
158
+ console.log(`... and ${firstParty.length - 15} more scripts`);
159
+ }
160
+ }
161
+ console.groupEnd();
162
+
163
+ // Third-party details
164
+ console.log("");
165
+ console.group(`%c🌐 Third-Party Scripts (${thirdParty.length})`, "color: #ef4444; font-weight: bold;");
166
+
167
+ if (thirdParty.length === 0) {
168
+ console.log("%cāœ… No third-party scripts!", "color: #22c55e; font-weight: bold;");
169
+ } else {
170
+ const corsRestricted = thirdStats.withoutTiming;
171
+ if (corsRestricted > 0) {
172
+ console.log(`%cāš ļø ${corsRestricted} script(s) restricted by CORS (no Timing-Allow-Origin header)`, "color: #f59e0b;");
173
+ console.log(" Only total duration available for these scripts.");
174
+ console.log("");
175
+ }
176
+
177
+ const maxTotal = Math.max(...thirdParty.map((s) => s.total));
178
+
179
+ const tableData = thirdParty
180
+ .sort((a, b) => b.total - a.total)
181
+ .slice(0, 15)
182
+ .map((s) => ({
183
+ Script: s.shortName,
184
+ Host: s.host.length > 25 ? s.host.slice(0, 22) + "..." : s.host,
185
+ DNS: formatMs(s.dns),
186
+ TCP: formatMs(s.tcp),
187
+ TLS: formatMs(s.tls),
188
+ Request: formatMs(s.request),
189
+ Response: formatMs(s.response),
190
+ Total: formatMs(s.total),
191
+ "": formatBar(s.total, maxTotal),
192
+ }));
193
+
194
+ console.table(tableData);
195
+
196
+ if (thirdParty.length > 15) {
197
+ console.log(`... and ${thirdParty.length - 15} more scripts`);
198
+ }
199
+ }
200
+ console.groupEnd();
201
+
202
+ // Identify slow scripts
203
+ const slowThreshold = 500; // ms
204
+ const slowScripts = scripts.filter((s) => s.total > slowThreshold);
205
+
206
+ if (slowScripts.length > 0) {
207
+ console.log("");
208
+ console.group(`%c🐌 Slow Scripts (>${slowThreshold}ms)`, "color: #ef4444; font-weight: bold;");
209
+
210
+ slowScripts
211
+ .sort((a, b) => b.total - a.total)
212
+ .forEach((s) => {
213
+ const party = s.firstParty ? "1st" : "3rd";
214
+ const phases = [];
215
+ if (s.dns > 50) phases.push(`DNS: ${formatMs(s.dns)}`);
216
+ if (s.tcp > 50) phases.push(`TCP: ${formatMs(s.tcp)}`);
217
+ if (s.tls > 50) phases.push(`TLS: ${formatMs(s.tls)}`);
218
+ if (s.request > 100) phases.push(`Request: ${formatMs(s.request)}`);
219
+ if (s.response > 200) phases.push(`Response: ${formatMs(s.response)}`);
220
+
221
+ console.log(`${party} ${formatMs(s.total).padStart(8)} - ${s.shortName}`);
222
+ if (phases.length > 0 && s.hasTiming) {
223
+ console.log(`%c Slow phases: ${phases.join(", ")}`, "color: #666;");
224
+ }
225
+ });
226
+
227
+ console.groupEnd();
228
+ }
229
+
230
+ // Recommendations
231
+ const hasSlowDns = scripts.some((s) => s.dns > 100);
232
+ const hasSlowTcp = scripts.some((s) => s.tcp > 100);
233
+ const hasSlowRequest = scripts.some((s) => s.request > 200);
234
+ const hasCorsIssues = thirdStats.withoutTiming > 0;
235
+
236
+ if (hasSlowDns || hasSlowTcp || hasSlowRequest || hasCorsIssues) {
237
+ console.log("");
238
+ console.group("%cšŸ“ Recommendations", "color: #3b82f6; font-weight: bold;");
239
+
240
+ if (hasSlowDns) {
241
+ console.log("");
242
+ console.log("%cšŸ” Slow DNS lookups detected:", "font-weight: bold;");
243
+ console.log(" • Use <link rel='dns-prefetch'> for third-party domains");
244
+ console.log(" • Consider using a faster DNS provider");
245
+ }
246
+
247
+ if (hasSlowTcp) {
248
+ console.log("");
249
+ console.log("%cšŸ”Œ Slow TCP connections detected:", "font-weight: bold;");
250
+ console.log(" • Use <link rel='preconnect'> for critical third-party origins");
251
+ console.log(" • Enable HTTP/2 or HTTP/3 for multiplexing");
252
+ }
253
+
254
+ if (hasSlowRequest) {
255
+ console.log("");
256
+ console.log("%cšŸ“¤ Slow server response times detected:", "font-weight: bold;");
257
+ console.log(" • Optimize server processing");
258
+ console.log(" • Use a CDN closer to users");
259
+ console.log(" • Consider self-hosting critical third-party scripts");
260
+ }
261
+
262
+ if (hasCorsIssues) {
263
+ console.log("");
264
+ console.log("%cšŸ”’ CORS-restricted timing data:", "font-weight: bold;");
265
+ console.log(" • Third-party servers need Timing-Allow-Origin header");
266
+ console.log(" • Contact vendors to enable resource timing");
267
+ console.log(" • Self-host scripts to get full timing data");
268
+ }
269
+
270
+ console.groupEnd();
271
+ }
272
+
273
+ console.groupEnd();
274
+
275
+ return {
276
+ script: "First-And-Third-Party-Script-Timings",
277
+ status: "ok",
278
+ count: scripts.length,
279
+ details: {
280
+ firstPartyCount: firstParty.length,
281
+ thirdPartyCount: thirdParty.length,
282
+ corsRestrictedCount: thirdStats.withoutTiming,
283
+ slowScriptCount: slowScripts.length,
284
+ firstPartyAvgTotalMs: Math.round(firstStats.stats.total?.avg || 0),
285
+ thirdPartyAvgTotalMs: Math.round(thirdStats.stats.total?.avg || 0),
286
+ },
287
+ items: scripts.map(s => ({ shortName: s.shortName, host: s.host, firstParty: s.firstParty, totalMs: Math.round(s.total), dnsMs: Math.round(s.dns), tcpMs: Math.round(s.tcp), requestMs: Math.round(s.request), responseMs: Math.round(s.response), hasTiming: s.hasTiming })),
288
+ issues: [
289
+ ...(slowScripts.length > 0 ? [{ severity: "warning", message: `${slowScripts.length} script(s) take over ${slowThreshold}ms to load` }] : []),
290
+ ...(hasSlowDns ? [{ severity: "warning", message: "Slow DNS lookups detected (>100ms). Add dns-prefetch or preconnect." }] : []),
291
+ ...(hasSlowTcp ? [{ severity: "warning", message: "Slow TCP connections detected (>100ms). Add preconnect for critical origins." }] : []),
292
+ ...(hasSlowRequest ? [{ severity: "warning", message: "Slow server response times detected (>200ms). Consider CDN or self-hosting." }] : []),
293
+ ],
294
+ };
295
+ })();