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,317 @@
|
|
|
1
|
+
// Font Loading Analysis - Preloaded, Loaded, and Used Above The Fold
|
|
2
|
+
// https://webperf-snippets.nucliweb.net
|
|
3
|
+
|
|
4
|
+
(() => {
|
|
5
|
+
// Helper to extract font filename from URL
|
|
6
|
+
function getFontName(url) {
|
|
7
|
+
try {
|
|
8
|
+
const path = new URL(url).pathname;
|
|
9
|
+
return path.split("/").pop() || path;
|
|
10
|
+
} catch {
|
|
11
|
+
return url;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Check if URL is third-party
|
|
16
|
+
function isThirdParty(url) {
|
|
17
|
+
try {
|
|
18
|
+
return new URL(url).hostname !== location.hostname;
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Normalize font family name for comparison
|
|
25
|
+
function normalizeFontFamily(family) {
|
|
26
|
+
return family
|
|
27
|
+
.split(",")[0]
|
|
28
|
+
.trim()
|
|
29
|
+
.replace(/["']/g, "")
|
|
30
|
+
.toLowerCase();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 1. Get preloaded fonts
|
|
34
|
+
const preloadedFonts = Array.from(
|
|
35
|
+
document.querySelectorAll('link[rel="preload"][as="font"]')
|
|
36
|
+
).map((link) => ({
|
|
37
|
+
href: link.href,
|
|
38
|
+
name: getFontName(link.href),
|
|
39
|
+
crossorigin: link.crossOrigin,
|
|
40
|
+
type: link.type || "unknown",
|
|
41
|
+
thirdParty: isThirdParty(link.href),
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
// 2. Get loaded fonts from document.fonts API
|
|
45
|
+
const loadedFonts = Array.from(document.fonts.values())
|
|
46
|
+
.filter((font) => font.status === "loaded")
|
|
47
|
+
.map((font) => ({
|
|
48
|
+
family: font.family.replace(/["']/g, ""),
|
|
49
|
+
weight: font.weight,
|
|
50
|
+
style: font.style,
|
|
51
|
+
display: font.display || "unknown",
|
|
52
|
+
key: `${font.family.replace(/["']/g, "")}-${font.weight}-${font.style}`,
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
// Deduplicate loaded fonts
|
|
56
|
+
const uniqueLoadedFonts = Array.from(
|
|
57
|
+
new Map(loadedFonts.map((f) => [f.key, f])).values()
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// 3. Get fonts used above the fold
|
|
61
|
+
const viewportHeight = window.innerHeight;
|
|
62
|
+
const viewportWidth = window.innerWidth;
|
|
63
|
+
|
|
64
|
+
const aboveFoldElements = Array.from(
|
|
65
|
+
document.querySelectorAll("body *:not(script):not(style):not(link):not(source)")
|
|
66
|
+
).filter((el) => {
|
|
67
|
+
const rect = el.getBoundingClientRect();
|
|
68
|
+
return (
|
|
69
|
+
rect.top < viewportHeight &&
|
|
70
|
+
rect.bottom > 0 &&
|
|
71
|
+
rect.left < viewportWidth &&
|
|
72
|
+
rect.right > 0 &&
|
|
73
|
+
rect.width > 0 &&
|
|
74
|
+
rect.height > 0
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const usedFontsMap = new Map();
|
|
79
|
+
aboveFoldElements.forEach((el) => {
|
|
80
|
+
const style = getComputedStyle(el);
|
|
81
|
+
const family = style.fontFamily;
|
|
82
|
+
const weight = style.fontWeight;
|
|
83
|
+
const fontStyle = style.fontStyle;
|
|
84
|
+
const key = `${family}-${weight}-${fontStyle}`;
|
|
85
|
+
|
|
86
|
+
if (!usedFontsMap.has(key)) {
|
|
87
|
+
usedFontsMap.set(key, {
|
|
88
|
+
family: family.split(",")[0].trim().replace(/["']/g, ""),
|
|
89
|
+
fullFamily: family,
|
|
90
|
+
weight,
|
|
91
|
+
style: fontStyle,
|
|
92
|
+
elements: 1,
|
|
93
|
+
});
|
|
94
|
+
} else {
|
|
95
|
+
usedFontsMap.get(key).elements++;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const usedFonts = Array.from(usedFontsMap.values());
|
|
100
|
+
|
|
101
|
+
// 4. Analysis - find mismatches
|
|
102
|
+
const preloadedNames = preloadedFonts.map((f) =>
|
|
103
|
+
normalizeFontFamily(f.name.replace(/\.(woff2?|ttf|otf|eot)$/i, ""))
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const usedFamilies = usedFonts.map((f) => normalizeFontFamily(f.family));
|
|
107
|
+
|
|
108
|
+
// Fonts preloaded but not used above the fold
|
|
109
|
+
const preloadedNotUsed = preloadedFonts.filter((f) => {
|
|
110
|
+
const name = normalizeFontFamily(
|
|
111
|
+
f.name.replace(/\.(woff2?|ttf|otf|eot)$/i, "")
|
|
112
|
+
);
|
|
113
|
+
return !usedFamilies.some(
|
|
114
|
+
(used) => used.includes(name) || name.includes(used)
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Fonts used but not preloaded (potential optimization)
|
|
119
|
+
const usedNotPreloaded = usedFonts.filter((f) => {
|
|
120
|
+
const family = normalizeFontFamily(f.family);
|
|
121
|
+
// Exclude system fonts
|
|
122
|
+
const systemFonts = [
|
|
123
|
+
"arial",
|
|
124
|
+
"helvetica",
|
|
125
|
+
"times",
|
|
126
|
+
"georgia",
|
|
127
|
+
"verdana",
|
|
128
|
+
"system-ui",
|
|
129
|
+
"-apple-system",
|
|
130
|
+
"segoe ui",
|
|
131
|
+
"roboto",
|
|
132
|
+
"sans-serif",
|
|
133
|
+
"serif",
|
|
134
|
+
"monospace",
|
|
135
|
+
];
|
|
136
|
+
if (systemFonts.some((sf) => family.includes(sf))) return false;
|
|
137
|
+
return !preloadedNames.some(
|
|
138
|
+
(preloaded) => preloaded.includes(family) || family.includes(preloaded)
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Display results
|
|
143
|
+
console.group("%cš¤ Font Loading Analysis", "font-weight: bold; font-size: 14px;");
|
|
144
|
+
|
|
145
|
+
// Summary
|
|
146
|
+
console.log("");
|
|
147
|
+
console.log("%cSummary:", "font-weight: bold;");
|
|
148
|
+
console.log(` Preloaded fonts: ${preloadedFonts.length}`);
|
|
149
|
+
console.log(` Loaded fonts: ${uniqueLoadedFonts.length}`);
|
|
150
|
+
console.log(` Used above the fold: ${usedFonts.length}`);
|
|
151
|
+
|
|
152
|
+
// Preloaded fonts
|
|
153
|
+
console.log("");
|
|
154
|
+
console.group(
|
|
155
|
+
`%cā¬ļø Preloaded Fonts (${preloadedFonts.length})`,
|
|
156
|
+
"color: #8b5cf6; font-weight: bold;"
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
if (preloadedFonts.length === 0) {
|
|
160
|
+
console.log("No fonts preloaded via <link rel='preload'>.");
|
|
161
|
+
} else {
|
|
162
|
+
const preloadTable = preloadedFonts.map((f) => ({
|
|
163
|
+
Font: f.name,
|
|
164
|
+
Type: f.type,
|
|
165
|
+
"Third-Party": f.thirdParty ? "Yes" : "No",
|
|
166
|
+
Crossorigin: f.crossorigin || "missing ā ļø",
|
|
167
|
+
}));
|
|
168
|
+
console.table(preloadTable);
|
|
169
|
+
|
|
170
|
+
// Check for missing crossorigin
|
|
171
|
+
const missingCrossorigin = preloadedFonts.filter((f) => !f.crossorigin);
|
|
172
|
+
if (missingCrossorigin.length > 0) {
|
|
173
|
+
console.log(
|
|
174
|
+
"%cā ļø Fonts preloaded without crossorigin attribute will be fetched twice!",
|
|
175
|
+
"color: #f59e0b;"
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
console.groupEnd();
|
|
180
|
+
|
|
181
|
+
// Loaded fonts
|
|
182
|
+
console.log("");
|
|
183
|
+
console.group(
|
|
184
|
+
`%cš¦ Loaded Fonts (${uniqueLoadedFonts.length})`,
|
|
185
|
+
"color: #3b82f6; font-weight: bold;"
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
if (uniqueLoadedFonts.length === 0) {
|
|
189
|
+
console.log("No web fonts loaded (using system fonts only).");
|
|
190
|
+
} else {
|
|
191
|
+
const loadedTable = uniqueLoadedFonts.map((f) => ({
|
|
192
|
+
Family: f.family,
|
|
193
|
+
Weight: f.weight,
|
|
194
|
+
Style: f.style,
|
|
195
|
+
Display: f.display,
|
|
196
|
+
}));
|
|
197
|
+
console.table(loadedTable);
|
|
198
|
+
|
|
199
|
+
// Check font-display
|
|
200
|
+
const autoDisplay = uniqueLoadedFonts.filter(
|
|
201
|
+
(f) => f.display === "auto" || f.display === "unknown"
|
|
202
|
+
);
|
|
203
|
+
if (autoDisplay.length > 0) {
|
|
204
|
+
console.log(
|
|
205
|
+
`%cš” ${autoDisplay.length} font(s) using default font-display. Consider using 'swap' or 'optional'.`,
|
|
206
|
+
"color: #f59e0b;"
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
console.groupEnd();
|
|
211
|
+
|
|
212
|
+
// Used above the fold
|
|
213
|
+
console.log("");
|
|
214
|
+
console.group(
|
|
215
|
+
`%cšļø Fonts Used Above The Fold (${usedFonts.length})`,
|
|
216
|
+
"color: #22c55e; font-weight: bold;"
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
if (usedFonts.length === 0) {
|
|
220
|
+
console.log("No text elements found above the fold.");
|
|
221
|
+
} else {
|
|
222
|
+
const usedTable = usedFonts
|
|
223
|
+
.sort((a, b) => b.elements - a.elements)
|
|
224
|
+
.map((f) => ({
|
|
225
|
+
Family: f.family,
|
|
226
|
+
Weight: f.weight,
|
|
227
|
+
Style: f.style,
|
|
228
|
+
"Elements Using": f.elements,
|
|
229
|
+
}));
|
|
230
|
+
console.table(usedTable);
|
|
231
|
+
}
|
|
232
|
+
console.groupEnd();
|
|
233
|
+
|
|
234
|
+
// Issues and recommendations
|
|
235
|
+
const hasIssues = preloadedNotUsed.length > 0 || usedNotPreloaded.length > 0;
|
|
236
|
+
|
|
237
|
+
if (hasIssues) {
|
|
238
|
+
console.log("");
|
|
239
|
+
console.group("%cā ļø Potential Issues", "color: #ef4444; font-weight: bold;");
|
|
240
|
+
|
|
241
|
+
if (preloadedNotUsed.length > 0) {
|
|
242
|
+
console.log("");
|
|
243
|
+
console.log(
|
|
244
|
+
`%cšļø Preloaded but NOT used above the fold (${preloadedNotUsed.length}):`,
|
|
245
|
+
"font-weight: bold;"
|
|
246
|
+
);
|
|
247
|
+
console.log(" These preloads may be wasting bandwidth:");
|
|
248
|
+
preloadedNotUsed.forEach((f) => {
|
|
249
|
+
console.log(` ⢠${f.name}`);
|
|
250
|
+
});
|
|
251
|
+
console.log("");
|
|
252
|
+
console.log(" Consider:");
|
|
253
|
+
console.log(" ⢠Removing the preload if font is only used below the fold");
|
|
254
|
+
console.log(" ⢠The font may be for a different viewport/breakpoint");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (usedNotPreloaded.length > 0) {
|
|
258
|
+
console.log("");
|
|
259
|
+
console.log(
|
|
260
|
+
`%cš Used above the fold but NOT preloaded (${usedNotPreloaded.length}):`,
|
|
261
|
+
"font-weight: bold;"
|
|
262
|
+
);
|
|
263
|
+
console.log(" These fonts could benefit from preloading:");
|
|
264
|
+
usedNotPreloaded.forEach((f) => {
|
|
265
|
+
console.log(` ⢠${f.family} (${f.weight})`);
|
|
266
|
+
});
|
|
267
|
+
console.log("");
|
|
268
|
+
console.log("%cExample preload:", "font-weight: bold;");
|
|
269
|
+
console.log(
|
|
270
|
+
'%c<link rel="preload" href="/fonts/font.woff2" as="font" type="font/woff2" crossorigin>',
|
|
271
|
+
"font-family: monospace; color: #22c55e;"
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
console.groupEnd();
|
|
276
|
+
} else if (preloadedFonts.length > 0 && usedFonts.length > 0) {
|
|
277
|
+
console.log("");
|
|
278
|
+
console.log(
|
|
279
|
+
"%cā
Font loading looks optimized! Preloaded fonts match usage above the fold.",
|
|
280
|
+
"color: #22c55e; font-weight: bold;"
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Best practices
|
|
285
|
+
console.log("");
|
|
286
|
+
console.group("%cš Font Loading Best Practices", "color: #3b82f6; font-weight: bold;");
|
|
287
|
+
console.log("");
|
|
288
|
+
console.log("1. Preload critical fonts used above the fold");
|
|
289
|
+
console.log("2. Use font-display: swap or optional");
|
|
290
|
+
console.log("3. Always include crossorigin attribute on font preloads");
|
|
291
|
+
console.log("4. Self-host fonts when possible for better control");
|
|
292
|
+
console.log("5. Subset fonts to include only needed characters");
|
|
293
|
+
console.log("6. Use WOFF2 format for best compression");
|
|
294
|
+
console.groupEnd();
|
|
295
|
+
|
|
296
|
+
console.groupEnd();
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
script: "Fonts-Preloaded-Loaded-and-used-above-the-fold",
|
|
300
|
+
status: "ok",
|
|
301
|
+
count: uniqueLoadedFonts.length,
|
|
302
|
+
details: {
|
|
303
|
+
preloadedCount: preloadedFonts.length,
|
|
304
|
+
loadedCount: uniqueLoadedFonts.length,
|
|
305
|
+
usedAboveFoldCount: usedFonts.length,
|
|
306
|
+
preloadedNotUsedCount: preloadedNotUsed.length,
|
|
307
|
+
usedNotPreloadedCount: usedNotPreloaded.length,
|
|
308
|
+
},
|
|
309
|
+
items: uniqueLoadedFonts.map(f => ({ family: f.family, weight: f.weight, style: f.style, display: f.display })),
|
|
310
|
+
usedFonts: usedFonts.map(f => ({ family: f.family, weight: f.weight, style: f.style, elements: f.elements })),
|
|
311
|
+
issues: [
|
|
312
|
+
...preloadedNotUsed.map(f => ({ severity: "warning", message: `Preloaded but not used above fold: ${f.name}` })),
|
|
313
|
+
...usedNotPreloaded.map(f => ({ severity: "warning", message: `Used above fold but not preloaded: ${f.family} (${f.weight})` })),
|
|
314
|
+
...preloadedFonts.filter(f => !f.crossorigin).map(f => ({ severity: "error", message: `Font preloaded without crossorigin (double fetch): ${f.name}` })),
|
|
315
|
+
],
|
|
316
|
+
};
|
|
317
|
+
})();
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// Inline CSS Analysis - Find and analyze all inline style tags
|
|
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 style tags
|
|
16
|
+
const styleTags = Array.from(document.querySelectorAll("style"));
|
|
17
|
+
|
|
18
|
+
if (styleTags.length === 0) {
|
|
19
|
+
console.group("%cš Inline CSS Analysis", "font-weight: bold; font-size: 14px;");
|
|
20
|
+
console.log("%cā
No inline <style> tags found.", "color: #22c55e; font-weight: bold;");
|
|
21
|
+
console.log("The page uses external stylesheets only.");
|
|
22
|
+
console.groupEnd();
|
|
23
|
+
return { script: "Inline-CSS-Info-and-Size", status: "ok", count: 0, items: [], issues: [] };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Analyze each style tag
|
|
27
|
+
const styles = styleTags.map((style, index) => {
|
|
28
|
+
const content = style.innerHTML;
|
|
29
|
+
const size = new Blob([content]).size;
|
|
30
|
+
const parent = style.parentElement?.tagName.toLowerCase() || "unknown";
|
|
31
|
+
const inHead = style.closest("head") !== null;
|
|
32
|
+
|
|
33
|
+
// Count approximate number of rules
|
|
34
|
+
const ruleCount = (content.match(/\{[^}]*\}/g) || []).length;
|
|
35
|
+
|
|
36
|
+
// Get preview (first 100 chars, cleaned up)
|
|
37
|
+
const preview = content
|
|
38
|
+
.replace(/\s+/g, " ")
|
|
39
|
+
.trim()
|
|
40
|
+
.slice(0, 80);
|
|
41
|
+
|
|
42
|
+
// Check for media queries
|
|
43
|
+
const hasMediaQueries = /@media/i.test(content);
|
|
44
|
+
|
|
45
|
+
// Check for keyframes (animation)
|
|
46
|
+
const hasKeyframes = /@keyframes/i.test(content);
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
index: index + 1,
|
|
50
|
+
element: style,
|
|
51
|
+
size,
|
|
52
|
+
sizeKB: formatKB(size),
|
|
53
|
+
parent,
|
|
54
|
+
inHead,
|
|
55
|
+
ruleCount,
|
|
56
|
+
preview: preview + (content.length > 80 ? "..." : ""),
|
|
57
|
+
hasMediaQueries,
|
|
58
|
+
hasKeyframes,
|
|
59
|
+
content,
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Calculate totals and find issues
|
|
64
|
+
const totalSize = styles.reduce((sum, s) => sum + s.size, 0);
|
|
65
|
+
const totalRules = styles.reduce((sum, s) => sum + s.ruleCount, 0);
|
|
66
|
+
const criticalBudget = 14 * 1024; // 14 KB
|
|
67
|
+
|
|
68
|
+
// Find duplicated content
|
|
69
|
+
const contentMap = new Map();
|
|
70
|
+
styles.forEach((s) => {
|
|
71
|
+
const normalized = s.content.replace(/\s+/g, " ").trim();
|
|
72
|
+
if (!contentMap.has(normalized)) {
|
|
73
|
+
contentMap.set(normalized, []);
|
|
74
|
+
}
|
|
75
|
+
contentMap.get(normalized).push(s.index);
|
|
76
|
+
});
|
|
77
|
+
const duplicates = Array.from(contentMap.entries())
|
|
78
|
+
.filter(([, indices]) => indices.length > 1)
|
|
79
|
+
.map(([, indices]) => indices);
|
|
80
|
+
|
|
81
|
+
// Rating based on total size
|
|
82
|
+
let rating, ratingColor;
|
|
83
|
+
if (totalSize <= criticalBudget * 0.5) {
|
|
84
|
+
rating = "Good";
|
|
85
|
+
ratingColor = "#22c55e";
|
|
86
|
+
} else if (totalSize <= criticalBudget) {
|
|
87
|
+
rating = "Acceptable";
|
|
88
|
+
ratingColor = "#f59e0b";
|
|
89
|
+
} else {
|
|
90
|
+
rating = "Too Large";
|
|
91
|
+
ratingColor = "#ef4444";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Display results
|
|
95
|
+
console.group("%cš Inline CSS Analysis", "font-weight: bold; font-size: 14px;");
|
|
96
|
+
|
|
97
|
+
// Summary
|
|
98
|
+
console.log("");
|
|
99
|
+
console.log("%cSummary:", "font-weight: bold;");
|
|
100
|
+
console.log(` Total inline <style> tags: ${styles.length}`);
|
|
101
|
+
console.log(` Total size: ${formatBytes(totalSize)} (${formatKB(totalSize)} KB)`);
|
|
102
|
+
console.log(` Total CSS rules: ~${totalRules}`);
|
|
103
|
+
console.log(
|
|
104
|
+
` Budget used: ${((totalSize / criticalBudget) * 100).toFixed(0)}% of 14 KB`
|
|
105
|
+
);
|
|
106
|
+
console.log(` Rating: %c${rating}`, `color: ${ratingColor}; font-weight: bold;`);
|
|
107
|
+
|
|
108
|
+
// Visual budget bar
|
|
109
|
+
const budgetPct = Math.min((totalSize / criticalBudget) * 100, 100);
|
|
110
|
+
const barWidth = Math.round(budgetPct / 5);
|
|
111
|
+
const bar = "ā".repeat(barWidth) + "ā".repeat(20 - barWidth);
|
|
112
|
+
console.log(` [${bar}] ${budgetPct.toFixed(0)}%`);
|
|
113
|
+
|
|
114
|
+
// Details table
|
|
115
|
+
console.log("");
|
|
116
|
+
console.group(
|
|
117
|
+
`%cš Style Tags (${styles.length})`,
|
|
118
|
+
"color: #3b82f6; font-weight: bold;"
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const tableData = styles.map((s) => ({
|
|
122
|
+
"#": s.index,
|
|
123
|
+
Location: s.inHead ? "head" : "body",
|
|
124
|
+
Size: formatBytes(s.size),
|
|
125
|
+
Rules: s.ruleCount,
|
|
126
|
+
"Media Queries": s.hasMediaQueries ? "Yes" : "-",
|
|
127
|
+
Animations: s.hasKeyframes ? "Yes" : "-",
|
|
128
|
+
Preview: s.preview,
|
|
129
|
+
}));
|
|
130
|
+
console.table(tableData);
|
|
131
|
+
|
|
132
|
+
// Elements for inspection
|
|
133
|
+
console.log("");
|
|
134
|
+
console.log("%cš Elements for inspection:", "font-weight: bold;");
|
|
135
|
+
styles.forEach((s) => {
|
|
136
|
+
const warning = s.size > 5 * 1024 ? " ā ļø Large" : "";
|
|
137
|
+
console.log(`${s.index}. (${formatBytes(s.size)})${warning}`, s.element);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
console.groupEnd();
|
|
141
|
+
|
|
142
|
+
// Issues
|
|
143
|
+
const issues = [];
|
|
144
|
+
|
|
145
|
+
if (totalSize > criticalBudget) {
|
|
146
|
+
issues.push({
|
|
147
|
+
type: "size",
|
|
148
|
+
message: `Total inline CSS (${formatBytes(totalSize)}) exceeds 14 KB critical budget`,
|
|
149
|
+
suggestion: "Move non-critical styles to external stylesheet",
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (duplicates.length > 0) {
|
|
154
|
+
issues.push({
|
|
155
|
+
type: "duplicate",
|
|
156
|
+
message: `Found ${duplicates.length} duplicated style block(s)`,
|
|
157
|
+
details: duplicates.map((d) => `Tags #${d.join(", #")} have identical content`),
|
|
158
|
+
suggestion: "Consolidate duplicate styles into a single block",
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const largeBlocks = styles.filter((s) => s.size > 5 * 1024);
|
|
163
|
+
if (largeBlocks.length > 0) {
|
|
164
|
+
issues.push({
|
|
165
|
+
type: "large",
|
|
166
|
+
message: `${largeBlocks.length} style block(s) over 5 KB`,
|
|
167
|
+
details: largeBlocks.map((s) => `Tag #${s.index}: ${formatBytes(s.size)}`),
|
|
168
|
+
suggestion: "Consider moving large blocks to external cached stylesheets",
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const bodyStyles = styles.filter((s) => !s.inHead);
|
|
173
|
+
if (bodyStyles.length > 0) {
|
|
174
|
+
issues.push({
|
|
175
|
+
type: "location",
|
|
176
|
+
message: `${bodyStyles.length} style block(s) in <body>`,
|
|
177
|
+
details: bodyStyles.map((s) => `Tag #${s.index}`),
|
|
178
|
+
suggestion: "Move styles to <head> or use external stylesheets for better parsing",
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (issues.length > 0) {
|
|
183
|
+
console.log("");
|
|
184
|
+
console.group("%cā ļø Issues Found", "color: #ef4444; font-weight: bold;");
|
|
185
|
+
|
|
186
|
+
issues.forEach((issue) => {
|
|
187
|
+
console.log("");
|
|
188
|
+
console.log(`%c${issue.message}`, "font-weight: bold;");
|
|
189
|
+
if (issue.details) {
|
|
190
|
+
issue.details.forEach((d) => console.log(` ⢠${d}`));
|
|
191
|
+
}
|
|
192
|
+
console.log(` ā ${issue.suggestion}`);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
console.groupEnd();
|
|
196
|
+
} else {
|
|
197
|
+
console.log("");
|
|
198
|
+
console.log(
|
|
199
|
+
"%cā
No issues found. Inline CSS is within recommended limits.",
|
|
200
|
+
"color: #22c55e; font-weight: bold;"
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Recommendations
|
|
205
|
+
console.log("");
|
|
206
|
+
console.group("%cš Best Practices", "color: #3b82f6; font-weight: bold;");
|
|
207
|
+
console.log("");
|
|
208
|
+
console.log("⢠Keep critical CSS under 14 KB (fits in first TCP round-trip)");
|
|
209
|
+
console.log("⢠Inline only above-the-fold critical styles");
|
|
210
|
+
console.log("⢠Move large stylesheets to external files for caching");
|
|
211
|
+
console.log("⢠Avoid duplicate style blocks");
|
|
212
|
+
console.log("⢠Place <style> tags in <head> for optimal parsing");
|
|
213
|
+
console.log("");
|
|
214
|
+
console.log("%cExample critical CSS pattern:", "font-weight: bold;");
|
|
215
|
+
console.log('%c<head>\n <style>/* Critical above-fold CSS here */</style>\n <link rel="preload" href="main.css" as="style">\n <link rel="stylesheet" href="main.css" media="print" onload="this.media=\'all\'">\n</head>', "font-family: monospace; color: #22c55e;");
|
|
216
|
+
console.groupEnd();
|
|
217
|
+
|
|
218
|
+
console.groupEnd();
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
script: "Inline-CSS-Info-and-Size",
|
|
222
|
+
status: "ok",
|
|
223
|
+
count: styles.length,
|
|
224
|
+
details: {
|
|
225
|
+
totalSizeBytes: totalSize,
|
|
226
|
+
totalRules,
|
|
227
|
+
criticalBudgetBytes: criticalBudget,
|
|
228
|
+
duplicateBlocks: duplicates.length,
|
|
229
|
+
bodyStyleBlocks: styles.filter(s => !s.inHead).length,
|
|
230
|
+
},
|
|
231
|
+
items: styles.map(s => ({ index: s.index, sizeBytes: s.size, parent: s.parent, inHead: s.inHead, ruleCount: s.ruleCount, hasMediaQueries: s.hasMediaQueries, hasKeyframes: s.hasKeyframes })),
|
|
232
|
+
issues: issues.map(i => ({ severity: "warning", message: i.message })),
|
|
233
|
+
};
|
|
234
|
+
})();
|