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,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
|
+
})();
|