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,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
|