webperf-snippets 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +139 -0
  2. package/package.json +54 -0
  3. package/snippets/CoreWebVitals/CLS.js +88 -0
  4. package/snippets/CoreWebVitals/INP.js +432 -0
  5. package/snippets/CoreWebVitals/LCP-Image-Entropy.js +198 -0
  6. package/snippets/CoreWebVitals/LCP-Subparts.js +286 -0
  7. package/snippets/CoreWebVitals/LCP-Trail.js +184 -0
  8. package/snippets/CoreWebVitals/LCP-Video-Candidate.js +228 -0
  9. package/snippets/CoreWebVitals/LCP.js +135 -0
  10. package/snippets/CoreWebVitals/WORKFLOWS.md +203 -0
  11. package/snippets/DevTools-Overrides/Fetch-XHR-Timeline-inject.js +73 -0
  12. package/snippets/DevTools-Overrides/Fetch-XHR-Timeline-read.js +191 -0
  13. package/snippets/Interaction/Forced-Synchronous-Layout.js +286 -0
  14. package/snippets/Interaction/Input-Latency-Breakdown.js +199 -0
  15. package/snippets/Interaction/Interactions.js +220 -0
  16. package/snippets/Interaction/Layout-Shift-Loading-and-Interaction.js +266 -0
  17. package/snippets/Interaction/Long-Animation-Frames-Helpers.js +397 -0
  18. package/snippets/Interaction/Long-Animation-Frames-Script-Attribution.js +137 -0
  19. package/snippets/Interaction/Long-Animation-Frames.js +298 -0
  20. package/snippets/Interaction/LongTask.js +203 -0
  21. package/snippets/Interaction/Scroll-Performance.js +359 -0
  22. package/snippets/Interaction/WORKFLOWS.md +205 -0
  23. package/snippets/Loading/Back-Forward-Cache.js +473 -0
  24. package/snippets/Loading/CSS-Media-Queries-Analysis.js +512 -0
  25. package/snippets/Loading/Cache-Strategy-Analysis.js +808 -0
  26. package/snippets/Loading/Client-Side-Redirect-Detection.js +337 -0
  27. package/snippets/Loading/Content-Visibility.js +352 -0
  28. package/snippets/Loading/Critical-CSS-Detection.js +329 -0
  29. package/snippets/Loading/Event-Processing-Time.js +179 -0
  30. package/snippets/Loading/FCP.js +101 -0
  31. package/snippets/Loading/Find-Above-The-Fold-Lazy-Loaded-Images.js +264 -0
  32. package/snippets/Loading/Find-Images-With-Lazy-and-Fetchpriority.js +155 -0
  33. package/snippets/Loading/Find-non-Lazy-Loaded-Images-outside-of-the-viewport.js +300 -0
  34. package/snippets/Loading/Find-render-blocking-resources.js +161 -0
  35. package/snippets/Loading/First-And-Third-Party-Script-Info.js +269 -0
  36. package/snippets/Loading/First-And-Third-Party-Script-Timings.js +295 -0
  37. package/snippets/Loading/Fonts-Preloaded-Loaded-and-used-above-the-fold.js +317 -0
  38. package/snippets/Loading/Inline-CSS-Info-and-Size.js +234 -0
  39. package/snippets/Loading/Inline-Script-Info-and-Size.js +323 -0
  40. package/snippets/Loading/JS-Execution-Time-Breakdown.js +336 -0
  41. package/snippets/Loading/Prefetch-Resource-Validation.js +456 -0
  42. package/snippets/Loading/Priority-Hints-Audit.js +336 -0
  43. package/snippets/Loading/Resource-Hints-Validation.js +503 -0
  44. package/snippets/Loading/Resource-Hints.js +400 -0
  45. package/snippets/Loading/SSR-Hydration-Data-Analysis.js +364 -0
  46. package/snippets/Loading/Script-Loading.js +332 -0
  47. package/snippets/Loading/Service-Worker-Analysis.js +192 -0
  48. package/snippets/Loading/TTFB-Resources.js +115 -0
  49. package/snippets/Loading/TTFB-Sub-Parts.js +117 -0
  50. package/snippets/Loading/TTFB.js +37 -0
  51. package/snippets/Loading/Validate-Preload-Async-Defer-Scripts.js +394 -0
  52. package/snippets/Loading/WORKFLOWS.md +163 -0
  53. package/snippets/Media/Image-Element-Audit.js +350 -0
  54. package/snippets/Media/SVG-Embedded-Bitmap-Analysis.js +225 -0
  55. package/snippets/Media/Video-Element-Audit.js +282 -0
  56. package/snippets/Media/WORKFLOWS.md +190 -0
  57. package/snippets/README.md +98 -0
  58. package/snippets/Resources/Network-Bandwidth-Connection-Quality.js +103 -0
  59. package/snippets/Resources/WORKFLOWS.md +173 -0
  60. package/src/bin.js +229 -0
  61. package/src/decision-tree.js +19 -0
  62. package/src/load-snippet.js +23 -0
  63. package/src/reporters/human.js +179 -0
  64. package/src/reporters/json.js +3 -0
  65. package/src/runner.js +61 -0
  66. package/src/workflows/audit.js +21 -0
  67. package/src/workflows/cwv.js +10 -0
@@ -0,0 +1,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
+ })();