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,323 @@
|
|
|
1
|
+
// Inline Script Analysis - Find and analyze all inline scripts
|
|
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
|
+
const formatKB = (bytes) => (bytes / 1024).toFixed(2);
|
|
14
|
+
|
|
15
|
+
// Get all inline scripts (no src attribute)
|
|
16
|
+
const scriptTags = Array.from(
|
|
17
|
+
document.querySelectorAll("script:not([src])")
|
|
18
|
+
).filter((s) => s.innerHTML.trim().length > 0);
|
|
19
|
+
|
|
20
|
+
if (scriptTags.length === 0) {
|
|
21
|
+
console.group("%cš Inline Script Analysis", "font-weight: bold; font-size: 14px;");
|
|
22
|
+
console.log("%cā
No inline scripts found.", "color: #22c55e; font-weight: bold;");
|
|
23
|
+
console.log("The page uses external scripts only.");
|
|
24
|
+
console.groupEnd();
|
|
25
|
+
return { script: "Inline-Script-Info-and-Size", status: "ok", count: 0, items: [], issues: [] };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Analyze each script
|
|
29
|
+
const scripts = scriptTags.map((script, index) => {
|
|
30
|
+
const content = script.innerHTML;
|
|
31
|
+
const size = new Blob([content]).size;
|
|
32
|
+
const type = script.type || "text/javascript";
|
|
33
|
+
const inHead = script.closest("head") !== null;
|
|
34
|
+
|
|
35
|
+
// Determine script category
|
|
36
|
+
let category = "classic";
|
|
37
|
+
if (type === "module") {
|
|
38
|
+
category = "module";
|
|
39
|
+
} else if (type === "application/ld+json") {
|
|
40
|
+
category = "json-ld";
|
|
41
|
+
} else if (type.includes("template") || type === "text/html") {
|
|
42
|
+
category = "template";
|
|
43
|
+
} else if (type !== "text/javascript" && type !== "") {
|
|
44
|
+
category = "other";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Check if parser-blocking (classic scripts without async/defer)
|
|
48
|
+
const isParserBlocking =
|
|
49
|
+
category === "classic" &&
|
|
50
|
+
!script.hasAttribute("async") &&
|
|
51
|
+
!script.hasAttribute("defer");
|
|
52
|
+
|
|
53
|
+
// Get preview (first 80 chars, cleaned up)
|
|
54
|
+
const preview = content
|
|
55
|
+
.replace(/\s+/g, " ")
|
|
56
|
+
.trim()
|
|
57
|
+
.slice(0, 80);
|
|
58
|
+
|
|
59
|
+
// Check for common patterns
|
|
60
|
+
const hasGlobalVars = /^(var|let|const|window\.)/.test(content.trim());
|
|
61
|
+
const isConfig = /config|settings|options|__/i.test(content.slice(0, 200));
|
|
62
|
+
const isAnalytics = /gtag|analytics|gtm|fbq|_gaq/i.test(content);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
index: index + 1,
|
|
66
|
+
element: script,
|
|
67
|
+
size,
|
|
68
|
+
sizeKB: formatKB(size),
|
|
69
|
+
type,
|
|
70
|
+
category,
|
|
71
|
+
inHead,
|
|
72
|
+
isParserBlocking,
|
|
73
|
+
hasGlobalVars,
|
|
74
|
+
isConfig,
|
|
75
|
+
isAnalytics,
|
|
76
|
+
preview: preview + (content.length > 80 ? "..." : ""),
|
|
77
|
+
content,
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Separate by category
|
|
82
|
+
const executableScripts = scripts.filter(
|
|
83
|
+
(s) => s.category === "classic" || s.category === "module"
|
|
84
|
+
);
|
|
85
|
+
const jsonLdScripts = scripts.filter((s) => s.category === "json-ld");
|
|
86
|
+
const otherScripts = scripts.filter(
|
|
87
|
+
(s) => s.category === "template" || s.category === "other"
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Calculate totals for executable scripts
|
|
91
|
+
const totalSize = executableScripts.reduce((sum, s) => sum + s.size, 0);
|
|
92
|
+
const parserBlockingScripts = executableScripts.filter((s) => s.isParserBlocking);
|
|
93
|
+
const parserBlockingInHead = parserBlockingScripts.filter((s) => s.inHead);
|
|
94
|
+
|
|
95
|
+
// Find duplicated content
|
|
96
|
+
const contentMap = new Map();
|
|
97
|
+
executableScripts.forEach((s) => {
|
|
98
|
+
const normalized = s.content.replace(/\s+/g, " ").trim();
|
|
99
|
+
if (normalized.length > 50) {
|
|
100
|
+
// Only check substantial scripts
|
|
101
|
+
if (!contentMap.has(normalized)) {
|
|
102
|
+
contentMap.set(normalized, []);
|
|
103
|
+
}
|
|
104
|
+
contentMap.get(normalized).push(s.index);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
const duplicates = Array.from(contentMap.entries())
|
|
108
|
+
.filter(([, indices]) => indices.length > 1)
|
|
109
|
+
.map(([, indices]) => indices);
|
|
110
|
+
|
|
111
|
+
// Rating
|
|
112
|
+
let rating, ratingColor;
|
|
113
|
+
const inlineThreshold = 3 * 1024; // 3 KB recommended max
|
|
114
|
+
if (totalSize <= inlineThreshold && parserBlockingInHead.length === 0) {
|
|
115
|
+
rating = "Good";
|
|
116
|
+
ratingColor = "#22c55e";
|
|
117
|
+
} else if (totalSize <= inlineThreshold * 2 || parserBlockingInHead.length <= 2) {
|
|
118
|
+
rating = "Needs Review";
|
|
119
|
+
ratingColor = "#f59e0b";
|
|
120
|
+
} else {
|
|
121
|
+
rating = "Needs Optimization";
|
|
122
|
+
ratingColor = "#ef4444";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Display results
|
|
126
|
+
console.group("%cš Inline Script Analysis", "font-weight: bold; font-size: 14px;");
|
|
127
|
+
|
|
128
|
+
// Summary
|
|
129
|
+
console.log("");
|
|
130
|
+
console.log("%cSummary:", "font-weight: bold;");
|
|
131
|
+
console.log(` Total inline scripts: ${scripts.length}`);
|
|
132
|
+
console.log(` Executable scripts: ${executableScripts.length}`);
|
|
133
|
+
if (jsonLdScripts.length > 0) {
|
|
134
|
+
console.log(` JSON-LD (structured data): ${jsonLdScripts.length}`);
|
|
135
|
+
}
|
|
136
|
+
if (otherScripts.length > 0) {
|
|
137
|
+
console.log(` Templates/Other: ${otherScripts.length}`);
|
|
138
|
+
}
|
|
139
|
+
console.log(` Total executable size: ${formatBytes(totalSize)} (${formatKB(totalSize)} KB)`);
|
|
140
|
+
console.log(` Parser-blocking scripts: ${parserBlockingScripts.length}`);
|
|
141
|
+
if (parserBlockingInHead.length > 0) {
|
|
142
|
+
console.log(
|
|
143
|
+
` %cā ļø Parser-blocking in <head>: ${parserBlockingInHead.length}`,
|
|
144
|
+
"color: #ef4444;"
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
console.log(` Rating: %c${rating}`, `color: ${ratingColor}; font-weight: bold;`);
|
|
148
|
+
|
|
149
|
+
// Executable scripts table
|
|
150
|
+
if (executableScripts.length > 0) {
|
|
151
|
+
console.log("");
|
|
152
|
+
console.group(
|
|
153
|
+
`%cā” Executable Scripts (${executableScripts.length})`,
|
|
154
|
+
"color: #f59e0b; font-weight: bold;"
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const execTable = executableScripts
|
|
158
|
+
.sort((a, b) => b.size - a.size)
|
|
159
|
+
.map((s) => ({
|
|
160
|
+
"#": s.index,
|
|
161
|
+
Location: s.inHead ? "head" : "body",
|
|
162
|
+
Type: s.category,
|
|
163
|
+
Size: formatBytes(s.size),
|
|
164
|
+
"Parser-Blocking": s.isParserBlocking ? "ā ļø Yes" : "No",
|
|
165
|
+
Pattern: s.isAnalytics
|
|
166
|
+
? "Analytics"
|
|
167
|
+
: s.isConfig
|
|
168
|
+
? "Config"
|
|
169
|
+
: s.hasGlobalVars
|
|
170
|
+
? "Globals"
|
|
171
|
+
: "-",
|
|
172
|
+
Preview: s.preview,
|
|
173
|
+
}));
|
|
174
|
+
console.table(execTable);
|
|
175
|
+
|
|
176
|
+
// Elements for inspection
|
|
177
|
+
console.log("");
|
|
178
|
+
console.log("%cš Elements for inspection:", "font-weight: bold;");
|
|
179
|
+
executableScripts
|
|
180
|
+
.sort((a, b) => b.size - a.size)
|
|
181
|
+
.forEach((s) => {
|
|
182
|
+
const warnings = [];
|
|
183
|
+
if (s.isParserBlocking && s.inHead) warnings.push("blocking");
|
|
184
|
+
if (s.size > 2 * 1024) warnings.push("large");
|
|
185
|
+
const warningStr = warnings.length > 0 ? ` ā ļø ${warnings.join(", ")}` : "";
|
|
186
|
+
console.log(`${s.index}. (${formatBytes(s.size)})${warningStr}`, s.element);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
console.groupEnd();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// JSON-LD scripts
|
|
193
|
+
if (jsonLdScripts.length > 0) {
|
|
194
|
+
console.log("");
|
|
195
|
+
console.group(
|
|
196
|
+
`%cš JSON-LD Structured Data (${jsonLdScripts.length})`,
|
|
197
|
+
"color: #8b5cf6; font-weight: bold;"
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
const jsonTable = jsonLdScripts.map((s) => {
|
|
201
|
+
let schemaType = "-";
|
|
202
|
+
try {
|
|
203
|
+
const parsed = JSON.parse(s.content);
|
|
204
|
+
schemaType = parsed["@type"] || (Array.isArray(parsed) ? "Array" : "-");
|
|
205
|
+
} catch {}
|
|
206
|
+
return {
|
|
207
|
+
"#": s.index,
|
|
208
|
+
"Schema Type": schemaType,
|
|
209
|
+
Size: formatBytes(s.size),
|
|
210
|
+
};
|
|
211
|
+
});
|
|
212
|
+
console.table(jsonTable);
|
|
213
|
+
console.groupEnd();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Issues
|
|
217
|
+
const issues = [];
|
|
218
|
+
|
|
219
|
+
if (parserBlockingInHead.length > 0) {
|
|
220
|
+
issues.push({
|
|
221
|
+
type: "blocking",
|
|
222
|
+
message: `${parserBlockingInHead.length} parser-blocking script(s) in <head>`,
|
|
223
|
+
details: parserBlockingInHead.map(
|
|
224
|
+
(s) => `Script #${s.index}: ${formatBytes(s.size)}`
|
|
225
|
+
),
|
|
226
|
+
suggestion: "Move to end of <body> or add 'defer' attribute",
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const largeScripts = executableScripts.filter((s) => s.size > 2 * 1024);
|
|
231
|
+
if (largeScripts.length > 0) {
|
|
232
|
+
issues.push({
|
|
233
|
+
type: "large",
|
|
234
|
+
message: `${largeScripts.length} script(s) over 2 KB`,
|
|
235
|
+
details: largeScripts.map((s) => `Script #${s.index}: ${formatBytes(s.size)}`),
|
|
236
|
+
suggestion: "Consider moving large scripts to external cacheable files",
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (duplicates.length > 0) {
|
|
241
|
+
issues.push({
|
|
242
|
+
type: "duplicate",
|
|
243
|
+
message: `${duplicates.length} duplicated script(s) detected`,
|
|
244
|
+
details: duplicates.map((d) => `Scripts #${d.join(", #")} have identical content`),
|
|
245
|
+
suggestion: "Remove duplicate scripts or consolidate into one",
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const analyticsInHead = executableScripts.filter(
|
|
250
|
+
(s) => s.isAnalytics && s.inHead && s.isParserBlocking
|
|
251
|
+
);
|
|
252
|
+
if (analyticsInHead.length > 0) {
|
|
253
|
+
issues.push({
|
|
254
|
+
type: "analytics",
|
|
255
|
+
message: `${analyticsInHead.length} analytics script(s) blocking in <head>`,
|
|
256
|
+
details: analyticsInHead.map((s) => `Script #${s.index}`),
|
|
257
|
+
suggestion: "Load analytics scripts with 'async' or move to <body>",
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (issues.length > 0) {
|
|
262
|
+
console.log("");
|
|
263
|
+
console.group("%cā ļø Issues Found", "color: #ef4444; font-weight: bold;");
|
|
264
|
+
|
|
265
|
+
issues.forEach((issue) => {
|
|
266
|
+
console.log("");
|
|
267
|
+
console.log(`%c${issue.message}`, "font-weight: bold;");
|
|
268
|
+
if (issue.details) {
|
|
269
|
+
issue.details.forEach((d) => console.log(` ⢠${d}`));
|
|
270
|
+
}
|
|
271
|
+
console.log(` ā ${issue.suggestion}`);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
console.groupEnd();
|
|
275
|
+
} else if (executableScripts.length > 0) {
|
|
276
|
+
console.log("");
|
|
277
|
+
console.log(
|
|
278
|
+
"%cā
No major issues found. Inline scripts look reasonable.",
|
|
279
|
+
"color: #22c55e; font-weight: bold;"
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Best practices
|
|
284
|
+
console.log("");
|
|
285
|
+
console.group("%cš Best Practices", "color: #3b82f6; font-weight: bold;");
|
|
286
|
+
console.log("");
|
|
287
|
+
console.log("⢠Keep inline scripts small (< 1-2 KB)");
|
|
288
|
+
console.log("⢠Avoid parser-blocking scripts in <head>");
|
|
289
|
+
console.log("⢠Use 'defer' or 'async' for non-critical scripts");
|
|
290
|
+
console.log("⢠Move large scripts to external files for caching");
|
|
291
|
+
console.log("⢠Place non-critical scripts at end of <body>");
|
|
292
|
+
console.log("");
|
|
293
|
+
console.log("%cFor critical inline scripts:", "font-weight: bold;");
|
|
294
|
+
console.log(
|
|
295
|
+
'%c<script>\n // Small critical code only\n window.__CONFIG = { ... };\n</script>',
|
|
296
|
+
"font-family: monospace; color: #22c55e;"
|
|
297
|
+
);
|
|
298
|
+
console.log("");
|
|
299
|
+
console.log("%cFor larger scripts:", "font-weight: bold;");
|
|
300
|
+
console.log(
|
|
301
|
+
'%c<script src="app.js" defer></script>',
|
|
302
|
+
"font-family: monospace; color: #22c55e;"
|
|
303
|
+
);
|
|
304
|
+
console.groupEnd();
|
|
305
|
+
|
|
306
|
+
console.groupEnd();
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
script: "Inline-Script-Info-and-Size",
|
|
310
|
+
status: "ok",
|
|
311
|
+
count: scripts.length,
|
|
312
|
+
details: {
|
|
313
|
+
executableCount: executableScripts.length,
|
|
314
|
+
jsonLdCount: jsonLdScripts.length,
|
|
315
|
+
otherCount: otherScripts.length,
|
|
316
|
+
totalSizeBytes: totalSize,
|
|
317
|
+
parserBlockingCount: parserBlockingScripts.length,
|
|
318
|
+
parserBlockingInHeadCount: parserBlockingInHead.length,
|
|
319
|
+
},
|
|
320
|
+
items: executableScripts.map(s => ({ index: s.index, sizeBytes: s.size, category: s.category, inHead: s.inHead, isParserBlocking: s.isParserBlocking, isAnalytics: s.isAnalytics, isConfig: s.isConfig })),
|
|
321
|
+
issues: issues.map(i => ({ severity: i.type === "blocking" ? "error" : "warning", message: i.message })),
|
|
322
|
+
};
|
|
323
|
+
})();
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
// JavaScript Execution Time Breakdown
|
|
2
|
+
// https://webperf-snippets.nucliweb.net
|
|
3
|
+
|
|
4
|
+
(() => {
|
|
5
|
+
const formatMs = (ms) => (ms > 0 ? ms.toFixed(0) + "ms" : "-");
|
|
6
|
+
const formatBytes = (bytes) => {
|
|
7
|
+
if (!bytes || bytes === 0) return "-";
|
|
8
|
+
const k = 1024;
|
|
9
|
+
const sizes = ["B", "KB", "MB"];
|
|
10
|
+
const i = Math.floor(Math.log(Math.max(bytes, 1)) / Math.log(k));
|
|
11
|
+
return (bytes / Math.pow(k, i)).toFixed(1) + " " + sizes[i];
|
|
12
|
+
};
|
|
13
|
+
const formatBar = (value, max, width = 16) => {
|
|
14
|
+
if (value <= 0 || max <= 0) return "ā".repeat(width);
|
|
15
|
+
const filled = Math.min(Math.round((value / max) * width), width);
|
|
16
|
+
return "ā".repeat(filled) + "ā".repeat(width - filled);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// 1. Page load timeline
|
|
20
|
+
const nav = performance.getEntriesByType("navigation")[0];
|
|
21
|
+
const domInteractive = nav?.domInteractive || 0;
|
|
22
|
+
const domContentLoaded = nav?.domContentLoadedEventEnd || 0;
|
|
23
|
+
const loadEvent = nav?.loadEventEnd || 0;
|
|
24
|
+
// 2. Script resource timing (download phase)
|
|
25
|
+
const scriptResources = performance
|
|
26
|
+
.getEntriesByType("resource")
|
|
27
|
+
.filter((r) => r.initiatorType === "script")
|
|
28
|
+
.map((r) => {
|
|
29
|
+
let shortName;
|
|
30
|
+
try {
|
|
31
|
+
const url = new URL(r.name);
|
|
32
|
+
shortName = url.pathname.split("/").pop() || url.hostname;
|
|
33
|
+
} catch {
|
|
34
|
+
shortName = r.name.split("/").pop() || r.name;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const transferSize = r.transferSize || 0;
|
|
38
|
+
const decodedSize = r.decodedBodySize || 0;
|
|
39
|
+
const downloadDuration = Math.round(r.responseEnd - r.startTime);
|
|
40
|
+
|
|
41
|
+
// CORS-restricted: third-party scripts without Timing-Allow-Origin return 0 for sizes
|
|
42
|
+
const corsRestricted = transferSize === 0 && decodedSize === 0 && downloadDuration > 0;
|
|
43
|
+
|
|
44
|
+
// Parse time estimates based on V8 data (v8.dev/blog/cost-of-javascript-2019)
|
|
45
|
+
// Mobile (~1ms/KB): Moto G4 class device ā worst-case baseline
|
|
46
|
+
// Desktop (~0.3ms/KB): ~3x faster than mobile baseline
|
|
47
|
+
const estimatedParseMobile = Math.round(decodedSize / 1024);
|
|
48
|
+
const estimatedParseDesktop = Math.round(decodedSize / 1024 / 3);
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
name: r.name,
|
|
52
|
+
shortName: shortName.split("?")[0] || shortName,
|
|
53
|
+
startTime: r.startTime,
|
|
54
|
+
responseEnd: r.responseEnd,
|
|
55
|
+
downloadDuration,
|
|
56
|
+
transferSize,
|
|
57
|
+
decodedSize,
|
|
58
|
+
corsRestricted,
|
|
59
|
+
estimatedParseMobile,
|
|
60
|
+
estimatedParseDesktop,
|
|
61
|
+
totalCostMs: downloadDuration + estimatedParseMobile,
|
|
62
|
+
};
|
|
63
|
+
})
|
|
64
|
+
.sort((a, b) => a.startTime - b.startTime);
|
|
65
|
+
|
|
66
|
+
// 3. Detect render-blocking scripts (in <head>, no async/defer/module)
|
|
67
|
+
const blockingSet = new Set();
|
|
68
|
+
Array.from(document.querySelectorAll("script[src]")).forEach((el) => {
|
|
69
|
+
const isModule = el.type === "module";
|
|
70
|
+
if (!el.async && !el.defer && !isModule && el.closest("head")) {
|
|
71
|
+
blockingSet.add(el.src);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const scripts = scriptResources.map((s) => ({
|
|
76
|
+
...s,
|
|
77
|
+
isBlocking: blockingSet.has(s.name),
|
|
78
|
+
}));
|
|
79
|
+
|
|
80
|
+
// 4. Summary calculations
|
|
81
|
+
const blocking = scripts.filter((s) => s.isBlocking);
|
|
82
|
+
const nonBlocking = scripts.filter((s) => !s.isBlocking);
|
|
83
|
+
const totalDownload = scripts.reduce((sum, s) => sum + s.downloadDuration, 0);
|
|
84
|
+
const totalEstParse = scripts.reduce((sum, s) => sum + s.estimatedParseMobile, 0);
|
|
85
|
+
const totalSize = scripts.reduce((sum, s) => sum + s.transferSize, 0);
|
|
86
|
+
const totalDecodedSize = scripts.reduce((sum, s) => sum + s.decodedSize, 0);
|
|
87
|
+
|
|
88
|
+
// Estimate how much blocking scripts delay domInteractive
|
|
89
|
+
// The critical path ends when the last blocking script finishes download + parse
|
|
90
|
+
const blockingCriticalPathEnd = blocking.reduce((max, s) => {
|
|
91
|
+
const estimatedDone = s.responseEnd + s.estimatedParseMobile;
|
|
92
|
+
return Math.max(max, estimatedDone);
|
|
93
|
+
}, 0);
|
|
94
|
+
|
|
95
|
+
const jsBlockingDelay =
|
|
96
|
+
blocking.length > 0 && domInteractive > 0
|
|
97
|
+
? Math.min(blockingCriticalPathEnd, domInteractive)
|
|
98
|
+
: 0;
|
|
99
|
+
|
|
100
|
+
// Large script threshold (50KB compressed = good candidate for splitting)
|
|
101
|
+
const SPLIT_THRESHOLD = 50 * 1024;
|
|
102
|
+
const splitCandidates = scripts.filter((s) => s.decodedSize > SPLIT_THRESHOLD);
|
|
103
|
+
|
|
104
|
+
// 5. Output
|
|
105
|
+
console.group(
|
|
106
|
+
"%cā” JavaScript Execution Time Breakdown",
|
|
107
|
+
"font-weight: bold; font-size: 14px;"
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// Page load timeline
|
|
111
|
+
console.log("");
|
|
112
|
+
console.log("%cš
Page Load Timeline:", "font-weight: bold;");
|
|
113
|
+
|
|
114
|
+
if (domInteractive > 0) {
|
|
115
|
+
const timelineMax = loadEvent || domContentLoaded || domInteractive;
|
|
116
|
+
const rows = [
|
|
117
|
+
{ label: "Navigation start", time: 0 },
|
|
118
|
+
{ label: "DOM Interactive", time: domInteractive },
|
|
119
|
+
{ label: "DOM Content Loaded", time: domContentLoaded },
|
|
120
|
+
{ label: "Load event", time: loadEvent },
|
|
121
|
+
].filter((r) => r.time > 0);
|
|
122
|
+
|
|
123
|
+
rows.forEach(({ label, time }) => {
|
|
124
|
+
const bar = formatBar(time, timelineMax, 20);
|
|
125
|
+
console.log(` ${label.padEnd(22)} ${formatMs(time).padStart(8)} ${bar}`);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (domInteractive > 0) {
|
|
129
|
+
const jsPct =
|
|
130
|
+
domInteractive > 0 ? ((jsBlockingDelay / domInteractive) * 100).toFixed(0) : 0;
|
|
131
|
+
console.log("");
|
|
132
|
+
console.log(
|
|
133
|
+
` JS blocking scripts contribute ~${jsPct}% of domInteractive (${formatMs(jsBlockingDelay)} est.)`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
console.log(" %cā ļø Navigation Timing unavailable", "color: #f59e0b;");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Script overview
|
|
141
|
+
console.log("");
|
|
142
|
+
console.log("%cš Script Overview:", "font-weight: bold;");
|
|
143
|
+
console.log(` Total scripts: ${scripts.length}`);
|
|
144
|
+
console.log(` Render-blocking: ${blocking.length}`);
|
|
145
|
+
console.log(` Non-blocking: ${nonBlocking.length}`);
|
|
146
|
+
console.log(` Total transfer size: ${formatBytes(totalSize)}`);
|
|
147
|
+
console.log(` Total decoded size: ${formatBytes(totalDecodedSize)}`);
|
|
148
|
+
console.log(` Compression ratio: ${totalDecodedSize > 0 && totalSize > 0 ? (totalDecodedSize / totalSize).toFixed(1) + "x" : "-"}`);
|
|
149
|
+
console.log("");
|
|
150
|
+
console.log("%c Cost Estimates (download + parse):", "color: #6b7280;");
|
|
151
|
+
console.log(` Total download time: ${formatMs(totalDownload)}`);
|
|
152
|
+
console.log(` Est. total parse time: ${formatMs(totalEstParse)} (~1ms/KB decoded)`);
|
|
153
|
+
console.log(` Est. total JS cost: ${formatMs(totalDownload + totalEstParse)}`);
|
|
154
|
+
|
|
155
|
+
// Per-script breakdown table
|
|
156
|
+
if (scripts.length > 0) {
|
|
157
|
+
console.log("");
|
|
158
|
+
console.group("%cš Per-Script Breakdown", "color: #3b82f6; font-weight: bold;");
|
|
159
|
+
|
|
160
|
+
console.log("%cš± Parse estimates: mobile ~1ms/KB Ā· desktop ~0.3ms/KB (source: v8.dev/blog/cost-of-javascript-2019)", "color: #6b7280;");
|
|
161
|
+
console.log("");
|
|
162
|
+
|
|
163
|
+
const corsCount = scripts.filter((s) => s.corsRestricted).length;
|
|
164
|
+
if (corsCount > 0) {
|
|
165
|
+
console.log(
|
|
166
|
+
`%cā ļø ${corsCount} script(s) show "CORS" ā third-party size data blocked by missing Timing-Allow-Origin header`,
|
|
167
|
+
"color: #f59e0b;"
|
|
168
|
+
);
|
|
169
|
+
console.log(" Parse columns unavailable for these scripts.");
|
|
170
|
+
console.log("");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const maxCost = Math.max(...scripts.map((s) => s.totalCostMs));
|
|
174
|
+
const tableData = scripts
|
|
175
|
+
.sort((a, b) => b.totalCostMs - a.totalCostMs)
|
|
176
|
+
.slice(0, 20)
|
|
177
|
+
.map((s) => {
|
|
178
|
+
const name = s.shortName.length > 35 ? s.shortName.slice(0, 32) + "..." : s.shortName;
|
|
179
|
+
return {
|
|
180
|
+
"": s.isBlocking ? "š“" : "ā
",
|
|
181
|
+
Script: name,
|
|
182
|
+
Download: formatMs(s.downloadDuration),
|
|
183
|
+
"Parse(mob)": s.corsRestricted ? "CORS" : formatMs(s.estimatedParseMobile),
|
|
184
|
+
"Parse(dsk)": s.corsRestricted ? "CORS" : formatMs(s.estimatedParseDesktop),
|
|
185
|
+
"Transfer": s.corsRestricted ? "CORS" : formatBytes(s.transferSize),
|
|
186
|
+
"Decoded": s.corsRestricted ? "CORS" : formatBytes(s.decodedSize),
|
|
187
|
+
"": formatBar(s.totalCostMs, maxCost),
|
|
188
|
+
};
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
console.table(tableData);
|
|
192
|
+
|
|
193
|
+
if (scripts.length > 20) {
|
|
194
|
+
console.log(` ... and ${scripts.length - 20} more scripts`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Critical bundle warning (> 1 MB decoded)
|
|
198
|
+
const criticalBundles = scripts.filter((s) => !s.corsRestricted && s.decodedSize > 1024 * 1024);
|
|
199
|
+
if (criticalBundles.length > 0) {
|
|
200
|
+
console.log("");
|
|
201
|
+
console.log("%cšØ Critically large bundles (> 1 MB decoded):", "color: #ef4444; font-weight: bold;");
|
|
202
|
+
criticalBundles
|
|
203
|
+
.sort((a, b) => b.decodedSize - a.decodedSize)
|
|
204
|
+
.forEach((s) => {
|
|
205
|
+
console.log(` ${s.shortName}`);
|
|
206
|
+
console.log(` Decoded: ${formatBytes(s.decodedSize)} | Mobile parse: ~${formatMs(s.estimatedParseMobile)} | Desktop parse: ~${formatMs(s.estimatedParseDesktop)}`);
|
|
207
|
+
console.log(" ā Split with dynamic import() ā only load what the current route needs");
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
console.groupEnd();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Blocking scripts TTI impact
|
|
215
|
+
if (blocking.length > 0) {
|
|
216
|
+
console.log("");
|
|
217
|
+
console.group(
|
|
218
|
+
`%cš¦ Render-Blocking Scripts (${blocking.length})`,
|
|
219
|
+
"color: #ef4444; font-weight: bold;"
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
blocking
|
|
223
|
+
.sort((a, b) => b.totalCostMs - a.totalCostMs)
|
|
224
|
+
.forEach((s) => {
|
|
225
|
+
const name = s.shortName.length > 40 ? s.shortName.slice(0, 37) + "..." : s.shortName;
|
|
226
|
+
console.log(
|
|
227
|
+
` š“ ${name}`
|
|
228
|
+
);
|
|
229
|
+
console.log(
|
|
230
|
+
` Download: ${formatMs(s.downloadDuration)} | Parse mob: ~${formatMs(s.estimatedParseMobile)} | Parse dsk: ~${formatMs(s.estimatedParseDesktop)} | Size: ${formatBytes(s.transferSize)}`
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
console.log("");
|
|
235
|
+
console.log(" These scripts block HTML parsing until fully downloaded and executed.");
|
|
236
|
+
console.log(" ā Add defer to execute after parsing without blocking.");
|
|
237
|
+
console.log(" ā Add async for independent scripts (analytics, ads).");
|
|
238
|
+
console.groupEnd();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Code splitting opportunities
|
|
242
|
+
if (splitCandidates.length > 0) {
|
|
243
|
+
console.log("");
|
|
244
|
+
console.group(
|
|
245
|
+
`%cāļø Code Splitting Candidates (${splitCandidates.length} scripts > 50KB decoded)`,
|
|
246
|
+
"color: #8b5cf6; font-weight: bold;"
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
splitCandidates
|
|
250
|
+
.sort((a, b) => b.decodedSize - a.decodedSize)
|
|
251
|
+
.forEach((s) => {
|
|
252
|
+
const name = s.shortName.length > 40 ? s.shortName.slice(0, 37) + "..." : s.shortName;
|
|
253
|
+
console.log(
|
|
254
|
+
` š¦ ${name}`
|
|
255
|
+
);
|
|
256
|
+
console.log(
|
|
257
|
+
` Decoded: ${formatBytes(s.decodedSize)} | Parse mob: ~${formatMs(s.estimatedParseMobile)} | Parse dsk: ~${formatMs(s.estimatedParseDesktop)} | Transfer: ${formatBytes(s.transferSize)}`
|
|
258
|
+
);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
console.log("");
|
|
262
|
+
console.log(" Large scripts increase parse time on every page load, even if cached.");
|
|
263
|
+
console.log(" ā Use dynamic import() to split at route or feature boundaries.");
|
|
264
|
+
console.log(" ā Enable tree-shaking to eliminate unused exports.");
|
|
265
|
+
console.log(" ā Consider module/nomodule pattern for modern browsers.");
|
|
266
|
+
console.groupEnd();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Recommendations
|
|
270
|
+
const hasHighParseTime = totalEstParse > 500;
|
|
271
|
+
const hasBlockingScripts = blocking.length > 0;
|
|
272
|
+
const compressionRatio = totalDecodedSize / Math.max(totalSize, 1);
|
|
273
|
+
const poorCompression = compressionRatio < 2 && totalSize > 10 * 1024;
|
|
274
|
+
|
|
275
|
+
console.log("");
|
|
276
|
+
console.group("%cš Recommendations", "color: #3b82f6; font-weight: bold;");
|
|
277
|
+
|
|
278
|
+
if (!hasBlockingScripts && !hasHighParseTime && splitCandidates.length === 0) {
|
|
279
|
+
console.log(" %cā
No critical JS issues found.", "color: #22c55e; font-weight: bold;");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (hasBlockingScripts) {
|
|
283
|
+
console.log(` š“ ${blocking.length} render-blocking script(s) delay the page:`);
|
|
284
|
+
console.log(" ā defer: safe for most scripts (runs after parsing, in order)");
|
|
285
|
+
console.log(" ā async: for independent scripts with no DOM dependency");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (hasHighParseTime) {
|
|
289
|
+
console.log(` ā ļø High estimated parse cost (~${formatMs(totalEstParse)}):`);
|
|
290
|
+
console.log(" ā Split bundles with dynamic import() at route boundaries");
|
|
291
|
+
console.log(" ā Lazy-load non-critical features on demand");
|
|
292
|
+
console.log(" ā Use module preload for critical chunks: <link rel='modulepreload'>");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (poorCompression) {
|
|
296
|
+
console.log(` š¦ Low compression ratio (${compressionRatio.toFixed(1)}x) for ${formatBytes(totalSize)} transferred:`);
|
|
297
|
+
console.log(" ā Enable Brotli compression on your server/CDN");
|
|
298
|
+
console.log(" ā Ensure minification is enabled in your build");
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
console.log("");
|
|
302
|
+
console.log("%c š” For measured execution time (not estimates):", "color: #6b7280;");
|
|
303
|
+
console.log(
|
|
304
|
+
" Run the Long Animation Frames Script Attribution snippet to measure"
|
|
305
|
+
);
|
|
306
|
+
console.log(" actual execution time during user interactions.");
|
|
307
|
+
|
|
308
|
+
console.groupEnd();
|
|
309
|
+
|
|
310
|
+
console.groupEnd();
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
script: "JS-Execution-Time-Breakdown",
|
|
314
|
+
status: "ok",
|
|
315
|
+
count: scripts.length,
|
|
316
|
+
details: {
|
|
317
|
+
blockingCount: blocking.length,
|
|
318
|
+
nonBlockingCount: nonBlocking.length,
|
|
319
|
+
totalTransferBytes: totalSize,
|
|
320
|
+
totalDecodedBytes: totalDecodedSize,
|
|
321
|
+
totalDownloadMs: Math.round(totalDownload),
|
|
322
|
+
totalEstParseMobileMs: Math.round(totalEstParse),
|
|
323
|
+
splitCandidatesCount: splitCandidates.length,
|
|
324
|
+
domInteractiveMs: Math.round(domInteractive),
|
|
325
|
+
domContentLoadedMs: Math.round(domContentLoaded),
|
|
326
|
+
loadEventMs: Math.round(loadEvent),
|
|
327
|
+
},
|
|
328
|
+
items: scripts.slice(0, 50).map(s => ({ shortName: s.shortName, isBlocking: s.isBlocking, downloadMs: Math.round(s.downloadDuration), estimatedParseMobileMs: s.estimatedParseMobile, transferBytes: s.transferSize, decodedBytes: s.decodedSize, corsRestricted: s.corsRestricted })),
|
|
329
|
+
issues: [
|
|
330
|
+
...(blocking.length > 0 ? [{ severity: "error", message: `${blocking.length} render-blocking script(s) delay HTML parsing` }] : []),
|
|
331
|
+
...(hasHighParseTime ? [{ severity: "warning", message: `High estimated parse cost (~${Math.round(totalEstParse)}ms on mobile)` }] : []),
|
|
332
|
+
...(splitCandidates.length > 0 ? [{ severity: "warning", message: `${splitCandidates.length} script(s) over 50KB decoded ā consider code splitting` }] : []),
|
|
333
|
+
...(poorCompression ? [{ severity: "warning", message: `Low compression ratio (${compressionRatio.toFixed(1)}x) ā enable Brotli/gzip` }] : []),
|
|
334
|
+
],
|
|
335
|
+
};
|
|
336
|
+
})();
|