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,512 @@
1
+ // Analyze CSS @media rules for viewports bigger than a specified breakpoint
2
+ // Default minWidth = 768 (px), but you can customize it
3
+
4
+ async function analyzeCSSMediaQueries(minWidth = 768) {
5
+ const stylesheets = [...document.styleSheets];
6
+ const inlineMediaQueries = [];
7
+ const fileMediaQueries = [];
8
+ let inlineTotalClasses = 0;
9
+ let inlineTotalProperties = 0;
10
+ let filesTotalClasses = 0;
11
+ let filesTotalProperties = 0;
12
+ let inlineTotalBytes = 0;
13
+ let filesTotalBytes = 0;
14
+ let corsBlockedCount = 0;
15
+
16
+ // Helper to check if media query targets bigger than specified breakpoint
17
+ function isBiggerThanBreakpoint(mediaText) {
18
+ if (!mediaText) return false;
19
+
20
+ // Check for min-width greater than specified breakpoint
21
+ const minWidthMatch = mediaText.match(/min-width:\s*(\d+)(px|em|rem)/i);
22
+ if (minWidthMatch) {
23
+ const value = parseInt(minWidthMatch[1]);
24
+ const unit = minWidthMatch[2].toLowerCase();
25
+
26
+ if (unit === "px" && value > minWidth) return true;
27
+ if (unit === "em" && value > minWidth / 16) return true; // Convert to em
28
+ if (unit === "rem" && value > minWidth / 16) return true; // Convert to rem
29
+ }
30
+
31
+ // Check for max-width to exclude (mobile-only queries)
32
+ const maxWidthMatch = mediaText.match(/max-width:\s*(\d+)(px|em|rem)/i);
33
+ if (maxWidthMatch && !minWidthMatch) {
34
+ return false; // max-width only queries are for smaller viewports
35
+ }
36
+
37
+ return false;
38
+ }
39
+
40
+ // Helper to count classes and properties in CSS text
41
+ function countClassesAndProperties(cssText) {
42
+ const classMatches = cssText.match(/\.[a-zA-Z0-9_-]+/g) || [];
43
+ const propertyMatches = cssText.match(/[a-z-]+\s*:/g) || [];
44
+
45
+ return {
46
+ classes: classMatches.length,
47
+ properties: propertyMatches.length,
48
+ };
49
+ }
50
+
51
+ // Helper to calculate byte size
52
+ function getByteSize(text) {
53
+ return new Blob([text]).size;
54
+ }
55
+
56
+ // Helper to format bytes
57
+ function formatBytes(bytes) {
58
+ if (bytes === 0) return "0 Bytes";
59
+ const k = 1024;
60
+ const sizes = ["Bytes", "KB", "MB"];
61
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
62
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
63
+ }
64
+
65
+ // Parse CSS text to find @media rules
66
+ function parseMediaQueriesFromCSS(cssText, source, isInline) {
67
+ // Improved regex to capture complete @media rules
68
+ const mediaRegex = /@media\s*([^{]+)\{((?:[^{}]|\{[^{}]*\})*)\}/g;
69
+ let match;
70
+
71
+ while ((match = mediaRegex.exec(cssText)) !== null) {
72
+ const mediaText = match[1].trim();
73
+ const mediaContent = match[2];
74
+
75
+ if (isBiggerThanBreakpoint(mediaText)) {
76
+ const counts = countClassesAndProperties(mediaContent);
77
+ const byteSize = getByteSize(match[0]);
78
+
79
+ const mediaQueryData = {
80
+ source: source,
81
+ mediaQuery: mediaText,
82
+ classes: counts.classes,
83
+ properties: counts.properties,
84
+ bytes: byteSize,
85
+ bytesFormatted: formatBytes(byteSize),
86
+ };
87
+
88
+ if (isInline) {
89
+ inlineMediaQueries.push(mediaQueryData);
90
+ inlineTotalClasses += counts.classes;
91
+ inlineTotalProperties += counts.properties;
92
+ inlineTotalBytes += byteSize;
93
+ } else {
94
+ fileMediaQueries.push(mediaQueryData);
95
+ filesTotalClasses += counts.classes;
96
+ filesTotalProperties += counts.properties;
97
+ filesTotalBytes += byteSize;
98
+ }
99
+ }
100
+ }
101
+ }
102
+
103
+ // Process stylesheets
104
+ for (let sheetIndex = 0; sheetIndex < stylesheets.length; sheetIndex++) {
105
+ const sheet = stylesheets[sheetIndex];
106
+ const isInline = !sheet.href;
107
+ const source = sheet.href || `<style> tag #${sheetIndex}`;
108
+
109
+ try {
110
+ // Try to access via cssRules first
111
+ const rules = sheet.cssRules || sheet.rules;
112
+ if (rules) {
113
+ [...rules].forEach((rule) => {
114
+ if (rule.type === CSSRule.MEDIA_RULE) {
115
+ const mediaText = rule.media.mediaText;
116
+ if (isBiggerThanBreakpoint(mediaText)) {
117
+ const cssText = rule.cssText;
118
+ const counts = countClassesAndProperties(cssText);
119
+ const byteSize = getByteSize(cssText);
120
+
121
+ const mediaQueryData = {
122
+ source: source,
123
+ mediaQuery: mediaText,
124
+ classes: counts.classes,
125
+ properties: counts.properties,
126
+ bytes: byteSize,
127
+ bytesFormatted: formatBytes(byteSize),
128
+ };
129
+
130
+ if (isInline) {
131
+ inlineMediaQueries.push(mediaQueryData);
132
+ inlineTotalClasses += counts.classes;
133
+ inlineTotalProperties += counts.properties;
134
+ inlineTotalBytes += byteSize;
135
+ } else {
136
+ fileMediaQueries.push(mediaQueryData);
137
+ filesTotalClasses += counts.classes;
138
+ filesTotalProperties += counts.properties;
139
+ filesTotalBytes += byteSize;
140
+ }
141
+ }
142
+ }
143
+ });
144
+ }
145
+ } catch {
146
+ // If CORS blocked, try to fetch the CSS
147
+ if (sheet.href) {
148
+ try {
149
+ const response = await fetch(sheet.href);
150
+ const cssText = await response.text();
151
+ parseMediaQueriesFromCSS(cssText, sheet.href, false);
152
+ } catch {
153
+ // Silently count CORS blocked files
154
+ corsBlockedCount++;
155
+ }
156
+ }
157
+ }
158
+ }
159
+
160
+ const totalBytes = inlineTotalBytes + filesTotalBytes;
161
+
162
+ // Display results
163
+ console.group(`šŸ“Š CSS Media Queries Analysis (min-width > ${minWidth}px)`);
164
+ console.log(`Total @media rules found: ${inlineMediaQueries.length + fileMediaQueries.length}`);
165
+ console.log(`Total classes: ${inlineTotalClasses + filesTotalClasses}`);
166
+ console.log(`Total properties: ${inlineTotalProperties + filesTotalProperties}`);
167
+ console.groupEnd();
168
+
169
+ // Mobile Savings Estimate
170
+ console.group("šŸ’¾ POTENTIAL MOBILE SAVINGS");
171
+ console.log(
172
+ `%cEstimated CSS bytes that mobile doesn't need: ${formatBytes(totalBytes)}`,
173
+ "font-weight: bold; color: #22c55e; font-size: 14px;",
174
+ );
175
+ console.log(` └─ From inline CSS: ${formatBytes(inlineTotalBytes)}`);
176
+ console.log(` └─ From external files: ${formatBytes(filesTotalBytes)}`);
177
+ console.log("");
178
+ console.log("šŸ’” By splitting these styles into separate files with media queries,");
179
+ console.log(" mobile users won't need to download, parse, or process this CSS.");
180
+ console.groupEnd();
181
+
182
+ // Inline CSS Results
183
+ console.group("šŸ”· INLINE CSS (<style> tags)");
184
+ console.log(`Media queries: ${inlineMediaQueries.length}`);
185
+ console.log(`Classes: ${inlineTotalClasses}`);
186
+ console.log(`Properties: ${inlineTotalProperties}`);
187
+ console.log(`Total size: ${formatBytes(inlineTotalBytes)}`);
188
+ if (inlineMediaQueries.length === 0) {
189
+ console.log(`No inline media queries found for viewports > ${minWidth}px.`);
190
+ }
191
+ console.groupEnd();
192
+
193
+ // External Files Results
194
+ console.group("šŸ“ EXTERNAL FILES (.css files)");
195
+ console.log(`Media queries: ${fileMediaQueries.length}`);
196
+ console.log(`Classes: ${filesTotalClasses}`);
197
+ console.log(`Properties: ${filesTotalProperties}`);
198
+ console.log(`Total size: ${formatBytes(filesTotalBytes)}`);
199
+ if (fileMediaQueries.length === 0) {
200
+ console.log(`No external file media queries found for viewports > ${minWidth}px.`);
201
+ }
202
+ console.groupEnd();
203
+
204
+ // CORS warning if applicable
205
+ if (corsBlockedCount > 0) {
206
+ console.group("āš ļø CORS LIMITATIONS");
207
+ console.log(
208
+ `%c${corsBlockedCount} external CSS file(s) could not be analyzed due to CORS restrictions.`,
209
+ "color: #f59e0b; font-weight: bold",
210
+ );
211
+ console.log("");
212
+ console.log("These files are loaded by the browser but cannot be read via JavaScript.");
213
+ console.log("The analysis above reflects only the CSS files that were accessible.");
214
+ console.log("");
215
+ console.log("šŸ’” To analyze CORS-blocked files:");
216
+ console.log(" • Download the CSS files manually and run the analysis locally");
217
+ console.log(" • Use Chrome DevTools Coverage tab to measure unused CSS");
218
+ console.log(" • Run this analysis from the same origin as the CSS files");
219
+ console.groupEnd();
220
+ }
221
+
222
+ return {
223
+ summary: {
224
+ total: {
225
+ mediaQueries: inlineMediaQueries.length + fileMediaQueries.length,
226
+ classes: inlineTotalClasses + filesTotalClasses,
227
+ properties: inlineTotalProperties + filesTotalProperties,
228
+ bytes: totalBytes,
229
+ bytesFormatted: formatBytes(totalBytes),
230
+ },
231
+ inline: {
232
+ mediaQueries: inlineMediaQueries.length,
233
+ classes: inlineTotalClasses,
234
+ properties: inlineTotalProperties,
235
+ bytes: inlineTotalBytes,
236
+ bytesFormatted: formatBytes(inlineTotalBytes),
237
+ },
238
+ files: {
239
+ mediaQueries: fileMediaQueries.length,
240
+ classes: filesTotalClasses,
241
+ properties: filesTotalProperties,
242
+ bytes: filesTotalBytes,
243
+ bytesFormatted: formatBytes(filesTotalBytes),
244
+ },
245
+ corsBlocked: corsBlockedCount,
246
+ },
247
+ details: {
248
+ inline: inlineMediaQueries,
249
+ files: fileMediaQueries,
250
+ },
251
+ };
252
+ }
253
+
254
+ // CSS Performance Impact Analyzer
255
+ // Estimates the real-world performance cost of unnecessary CSS on mobile devices
256
+
257
+ async function analyzeCSSPerformanceImpact(minWidth = 768) {
258
+ console.log("šŸ” Analyzing CSS performance impact...\n");
259
+
260
+ // First, run the media queries analysis
261
+ const mediaQueryResults = await analyzeCSSMediaQueries(minWidth);
262
+ const unnecessaryBytes = mediaQueryResults.summary.total.bytes;
263
+
264
+ // If no data was found, show early exit message
265
+ if (unnecessaryBytes === 0 && mediaQueryResults.summary.corsBlocked > 0) {
266
+ console.log(
267
+ "%cāš ļø Unable to perform performance analysis",
268
+ "color: #f59e0b; font-weight: bold; font-size: 14px",
269
+ );
270
+ console.log("");
271
+ console.log(`All CSS files (${mediaQueryResults.summary.corsBlocked}) are blocked by CORS.`);
272
+ console.log("No desktop-specific @media queries could be analyzed.");
273
+ console.log("");
274
+ console.log("Please try one of these alternatives:");
275
+ console.log(" • Run this script from the same domain as the CSS files");
276
+ console.log(" • Use Chrome DevTools Coverage tab for manual analysis");
277
+ console.log(" • Download CSS files and analyze them locally");
278
+ return null;
279
+ }
280
+
281
+ if (unnecessaryBytes === 0) {
282
+ console.log("%cāœ… Great news!", "color: #22c55e; font-weight: bold; font-size: 14px");
283
+ console.log("");
284
+ console.log("No desktop-specific CSS found (min-width > " + minWidth + "px).");
285
+ console.log("This site appears to be optimized for mobile-first delivery.");
286
+ return null;
287
+ }
288
+
289
+ // Device profiles based on real Chrome UX Report data
290
+ const deviceProfiles = {
291
+ "High-end (Pixel 7, iPhone 14)": {
292
+ cssParsingSpeed: 1.5, // MB/s
293
+ cssSelectorMatchingMultiplier: 1.0,
294
+ networkSpeed: 10, // Mbps (4G LTE)
295
+ description: "Top 25% devices",
296
+ },
297
+ "Mid-range (Moto G Power, iPhone SE)": {
298
+ cssParsingSpeed: 0.8, // MB/s
299
+ cssSelectorMatchingMultiplier: 1.8,
300
+ networkSpeed: 5, // Mbps (4G)
301
+ description: "Median mobile device",
302
+ },
303
+ "Low-end (Moto E, older devices)": {
304
+ cssParsingSpeed: 0.3, // MB/s
305
+ cssSelectorMatchingMultiplier: 3.5,
306
+ networkSpeed: 2, // Mbps (3G/slow 4G)
307
+ description: "Bottom 25% devices",
308
+ },
309
+ };
310
+
311
+ const totalClasses = mediaQueryResults.summary.total.classes;
312
+ const totalProperties = mediaQueryResults.summary.total.properties;
313
+
314
+ console.group("⚔ PERFORMANCE IMPACT ANALYSIS");
315
+ console.log(`Unnecessary CSS size: ${mediaQueryResults.summary.total.bytesFormatted}`);
316
+ console.log(`Classes to process: ${totalClasses}`);
317
+ console.log(`Properties to compute: ${totalProperties}\n`);
318
+
319
+ Object.entries(deviceProfiles).forEach(([deviceName, profile]) => {
320
+ console.group(`šŸ“± ${deviceName}`);
321
+ console.log(` ${profile.description}`);
322
+ console.log("");
323
+
324
+ // 1. Network download time
325
+ const downloadTimeMs = (unnecessaryBytes * 8) / (profile.networkSpeed * 1000);
326
+
327
+ // 2. CSS Parsing time (converting bytes to styles)
328
+ const parsingTimeMs = (unnecessaryBytes / (1024 * 1024) / profile.cssParsingSpeed) * 1000;
329
+
330
+ // 3. CSSOM construction (creating the CSS Object Model)
331
+ // Approximation: ~0.01ms per CSS property on mid-range devices
332
+ const cssomConstructionMs = totalProperties * 0.01 * profile.cssSelectorMatchingMultiplier;
333
+
334
+ // 4. Selector matching overhead
335
+ // Each class needs to be evaluated against the DOM (even if it doesn't match)
336
+ // ~0.005ms per selector on mid-range devices
337
+ const selectorMatchingMs = totalClasses * 0.005 * profile.cssSelectorMatchingMultiplier;
338
+
339
+ // 5. Style recalculation overhead during interactions
340
+ // More CSS rules = more time in each style recalc
341
+ const recalcOverheadMs = totalProperties * 0.002 * profile.cssSelectorMatchingMultiplier;
342
+
343
+ const totalBlockingTime = downloadTimeMs + parsingTimeMs + cssomConstructionMs;
344
+ const totalRuntimeOverhead = selectorMatchingMs + recalcOverheadMs;
345
+
346
+ console.log("🚦 Render-blocking impact:");
347
+ console.log(` • Network download: ${downloadTimeMs.toFixed(2)}ms`);
348
+ console.log(` • CSS parsing: ${parsingTimeMs.toFixed(2)}ms`);
349
+ console.log(` • CSSOM construction: ${cssomConstructionMs.toFixed(2)}ms`);
350
+ console.log(
351
+ ` šŸ“Š Total blocking time: %c${totalBlockingTime.toFixed(2)}ms`,
352
+ "font-weight: bold; color: #ef4444",
353
+ );
354
+ console.log("");
355
+
356
+ console.log("āš™ļø Runtime overhead (per page interaction):");
357
+ console.log(` • Selector matching: ${selectorMatchingMs.toFixed(2)}ms`);
358
+ console.log(` • Style recalculation: ${recalcOverheadMs.toFixed(2)}ms`);
359
+ console.log(
360
+ ` šŸ“Š Total per-interaction: %c${totalRuntimeOverhead.toFixed(2)}ms`,
361
+ "font-weight: bold; color: #f59e0b",
362
+ );
363
+ console.log("");
364
+
365
+ // Core Web Vitals impact estimation
366
+ console.log("šŸ“ˆ Core Web Vitals Impact:");
367
+
368
+ // FCP impact (part of blocking time)
369
+ const fcpImpact = totalBlockingTime * 0.6; // ~60% affects FCP
370
+ console.log(` • FCP delay: ~${fcpImpact.toFixed(0)}ms`);
371
+
372
+ // LCP impact (if there are images/text affected by these styles)
373
+ const lcpImpact = totalBlockingTime * 0.4;
374
+ console.log(` • LCP delay: ~${lcpImpact.toFixed(0)}ms`);
375
+
376
+ // INP impact (runtime overhead affects each interaction)
377
+ console.log(` • INP overhead: ~${totalRuntimeOverhead.toFixed(0)}ms per interaction`);
378
+
379
+ // TBT (Total Blocking Time) - time where main thread is blocked
380
+ const tbtContribution = Math.max(0, totalBlockingTime - 50); // Tasks >50ms contribute to TBT
381
+ console.log(` • TBT contribution: ~${tbtContribution.toFixed(0)}ms`);
382
+
383
+ console.groupEnd();
384
+ console.log("");
385
+ });
386
+
387
+ console.groupEnd();
388
+
389
+ // Savings summary
390
+ console.group("šŸ’° POTENTIAL SAVINGS");
391
+
392
+ const midRangeProfile = deviceProfiles["Mid-range (Moto G Power, iPhone SE)"];
393
+ const midRangeDownload = (unnecessaryBytes * 8) / (midRangeProfile.networkSpeed * 1000);
394
+ const midRangeParsing =
395
+ (unnecessaryBytes / (1024 * 1024) / midRangeProfile.cssParsingSpeed) * 1000;
396
+ const midRangeCSSOM = totalProperties * 0.01 * midRangeProfile.cssSelectorMatchingMultiplier;
397
+ const midRangeTotalBlocking = midRangeDownload + midRangeParsing + midRangeCSSOM;
398
+
399
+ console.log("For the median mobile user (mid-range device):");
400
+ console.log("");
401
+ console.log(
402
+ `%cāœ“ Eliminate ${midRangeTotalBlocking.toFixed(0)}ms of render-blocking time`,
403
+ "font-weight: bold; color: #22c55e; font-size: 13px",
404
+ );
405
+ console.log(
406
+ `%cāœ“ Reduce INP by ~${(totalProperties * 0.002 * midRangeProfile.cssSelectorMatchingMultiplier).toFixed(0)}ms per interaction`,
407
+ "font-weight: bold; color: #22c55e; font-size: 13px",
408
+ );
409
+ console.log(
410
+ `%cāœ“ Save ${mediaQueryResults.summary.total.bytesFormatted} of bandwidth`,
411
+ "font-weight: bold; color: #22c55e; font-size: 13px",
412
+ );
413
+ console.log("");
414
+
415
+ console.log("Implementation strategy:");
416
+ console.log("1. Split desktop-specific CSS into separate file(s)");
417
+ console.log(
418
+ '2. Load with media query: <link rel="stylesheet" href="desktop.css" media="(min-width: 768px)">',
419
+ );
420
+ console.log("3. Consider critical CSS inlining for above-the-fold mobile content");
421
+ console.log("");
422
+
423
+ // Business impact estimation
424
+ const fcpImprovement = midRangeTotalBlocking * 0.6;
425
+ console.log("šŸ“Š Estimated business impact:");
426
+ console.log(` • FCP improvement: ~${fcpImprovement.toFixed(0)}ms`);
427
+ console.log(" • 100ms FCP improvement ā‰ˆ 1% conversion increase (Google/Deloitte research)");
428
+ console.log(` • Potential conversion lift: ~${(fcpImprovement / 100).toFixed(2)}%`);
429
+
430
+ console.groupEnd();
431
+
432
+ // Memory impact
433
+ console.group("🧠 MEMORY IMPACT");
434
+
435
+ // CSSOM memory estimation
436
+ // Approximately: each CSS rule = ~1KB in memory (including selector, properties, computed values)
437
+ const totalMediaRules = mediaQueryResults.summary.total.mediaQueries;
438
+ const estimatedMemoryKB = totalMediaRules * 1.0; // 1KB per rule
439
+
440
+ console.log(`Estimated CSSOM memory overhead: ~${estimatedMemoryKB.toFixed(1)} KB`);
441
+ console.log(
442
+ `Total unnecessary memory allocation: ~${(unnecessaryBytes / 1024 + estimatedMemoryKB).toFixed(1)} KB`,
443
+ );
444
+ console.log("");
445
+ console.log("šŸ’” This memory stays allocated throughout the page lifecycle,");
446
+ console.log(" contributing to memory pressure on low-end devices.");
447
+
448
+ console.groupEnd();
449
+
450
+ // CORS disclaimer if applicable
451
+ if (mediaQueryResults.summary.corsBlocked > 0) {
452
+ console.log("");
453
+ console.log(
454
+ `%cāš ļø Note: ${mediaQueryResults.summary.corsBlocked} CSS file(s) blocked by CORS were not analyzed.`,
455
+ "color: #f59e0b; font-weight: bold",
456
+ );
457
+ console.log("The actual performance impact could be higher than shown above.");
458
+ }
459
+
460
+ // Return structured data
461
+ return {
462
+ unnecessaryBytes,
463
+ unnecessaryCss: mediaQueryResults.summary.total.bytesFormatted,
464
+ totalClasses,
465
+ totalProperties,
466
+ corsBlockedCount: mediaQueryResults.summary.corsBlocked,
467
+ deviceImpact: Object.entries(deviceProfiles).reduce((acc, [name, profile]) => {
468
+ const download = (unnecessaryBytes * 8) / (profile.networkSpeed * 1000);
469
+ const parsing = (unnecessaryBytes / (1024 * 1024) / profile.cssParsingSpeed) * 1000;
470
+ const cssom = totalProperties * 0.01 * profile.cssSelectorMatchingMultiplier;
471
+ const totalBlocking = download + parsing + cssom;
472
+ const runtime =
473
+ (totalClasses * 0.005 + totalProperties * 0.002) * profile.cssSelectorMatchingMultiplier;
474
+
475
+ acc[name] = {
476
+ renderBlockingTimeMs: totalBlocking,
477
+ runtimeOverheadMs: runtime,
478
+ fcpImpactMs: totalBlocking * 0.6,
479
+ lcpImpactMs: totalBlocking * 0.4,
480
+ inpOverheadMs: runtime,
481
+ };
482
+ return acc;
483
+ }, {}),
484
+ estimatedConversionLift: ((midRangeTotalBlocking * 0.6) / 100).toFixed(2) + "%",
485
+ };
486
+ }
487
+
488
+ window.analyzeCSSPerformanceImpact = analyzeCSSPerformanceImpact;
489
+
490
+ // Run with default breakpoint (768px)
491
+ (async () => {
492
+ const result = await analyzeCSSMediaQueries();
493
+ if (!result) {
494
+ return { script: "CSS-Media-Queries-Analysis", status: "error", error: "No analyzable CSS found (CORS blocked or no desktop media queries)" };
495
+ }
496
+ return {
497
+ script: "CSS-Media-Queries-Analysis",
498
+ status: "ok",
499
+ count: result.summary.total.mediaQueries,
500
+ details: {
501
+ total: result.summary.total,
502
+ inline: result.summary.inline,
503
+ files: result.summary.files,
504
+ corsBlockedCount: result.summary.corsBlocked,
505
+ },
506
+ items: [...result.details.inline, ...result.details.files],
507
+ };
508
+ })();
509
+
510
+ // Or customize the breakpoint:
511
+ // analyzeCSSMediaQueries(1024); // for desktop
512
+ // analyzeCSSMediaQueries(480); // for small tablets