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