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,337 @@
1
+ // Client-Side Redirect Detection
2
+ // https://webperf-snippets.nucliweb.net
3
+
4
+ (async () => {
5
+ const navEntries = performance.getEntriesByType('navigation');
6
+
7
+ if (navEntries.length === 0) {
8
+ console.log(
9
+ '%cāš ļø Navigation Timing not available',
10
+ 'color: #f59e0b; font-weight: bold;'
11
+ );
12
+ return { script: "Client-Side-Redirect-Detection", status: "unsupported", error: "Navigation Timing not available" };
13
+ }
14
+
15
+ const navEntry = navEntries[0];
16
+ const currentURL = new URL(window.location.href);
17
+ const resources = performance.getEntriesByType('resource');
18
+
19
+ // Check for document navigations (only navigation type, exclude iframes)
20
+ // Filter out third-party iframes (analytics, ads, etc.)
21
+ const documentNavigations = resources.filter(
22
+ (r) => {
23
+ if (r.initiatorType !== 'navigation') return false;
24
+
25
+ // Only count same-origin navigations as potential redirects
26
+ try {
27
+ const resourceURL = new URL(r.name);
28
+ return resourceURL.origin === currentURL.origin;
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
33
+ );
34
+
35
+ // Check for server-side redirects
36
+ const serverRedirects = navEntry.redirectCount || 0;
37
+ const redirectTime = navEntry.redirectEnd - navEntry.redirectStart;
38
+
39
+ // Detect client-side redirect patterns
40
+ const hasSPARouter = !!window.history?.state;
41
+ const historyLength = window.history.length;
42
+
43
+ // Check document.referrer for same-origin navigation
44
+ const referrer = document.referrer ? new URL(document.referrer) : null;
45
+ const sameOrigin = referrer && referrer.origin === currentURL.origin;
46
+ const referrerPath = referrer?.pathname || '';
47
+ const currentPath = currentURL.pathname;
48
+
49
+ // Detect redirect indicators
50
+ const hasRedirectParam =
51
+ currentURL.searchParams.has('redirect') ||
52
+ currentURL.searchParams.has('from') ||
53
+ currentURL.searchParams.has('origin');
54
+
55
+ console.group(
56
+ '%cšŸ”„ Client-Side Redirect Detection',
57
+ 'font-weight: bold; font-size: 14px;'
58
+ );
59
+
60
+ // Current page info
61
+ console.log('');
62
+ console.log('%cšŸ“ Current Page:', 'font-weight: bold;');
63
+ console.log(` URL: ${currentURL.href}`);
64
+ console.log(` Path: ${currentPath}`);
65
+ if (referrer) {
66
+ console.log(` Referrer: ${referrer.href}`);
67
+ console.log(` Referrer path: ${referrerPath}`);
68
+ } else {
69
+ console.log(' Referrer: (none - direct navigation or blocked)');
70
+ }
71
+
72
+ // Server-side redirects
73
+ console.log('');
74
+ console.log('%c🌐 Server-Side Redirects:', 'font-weight: bold;');
75
+ if (serverRedirects > 0) {
76
+ console.log(`%c āš ļø ${serverRedirects} redirect(s) detected`, 'color: #f59e0b;');
77
+ console.log(` Redirect time: ${redirectTime.toFixed(1)}ms`);
78
+ console.log(' šŸ’” Minimize redirect chains for better performance');
79
+ } else {
80
+ console.log('%c āœ… No server-side redirects', 'color: #22c55e;');
81
+ }
82
+
83
+ // Client-side navigation detection
84
+ console.log('');
85
+ console.log('%cšŸ“± Client-Side Navigation Indicators:', 'font-weight: bold;');
86
+
87
+ let hasClientRedirect = false;
88
+ const indicators = [];
89
+
90
+ // Check 1: Same-origin referrer with different path (potential redirect)
91
+ if (sameOrigin && referrerPath !== currentPath && referrerPath !== '') {
92
+ hasClientRedirect = true;
93
+ indicators.push({
94
+ type: 'Same-origin navigation',
95
+ from: referrerPath,
96
+ to: currentPath,
97
+ severity: 'warning'
98
+ });
99
+ }
100
+
101
+ // Check 2: Document navigations in resource timing
102
+ if (documentNavigations.length > 0) {
103
+ hasClientRedirect = true;
104
+ documentNavigations.forEach((nav) => {
105
+ indicators.push({
106
+ type: 'Document navigation',
107
+ url: nav.name,
108
+ duration: nav.duration.toFixed(1) + 'ms',
109
+ severity: 'error'
110
+ });
111
+ });
112
+ }
113
+
114
+ // Check 3: Redirect URL parameters
115
+ if (hasRedirectParam) {
116
+ indicators.push({
117
+ type: 'Redirect parameter in URL',
118
+ params: Array.from(currentURL.searchParams.entries())
119
+ .filter(([key]) =>
120
+ key.toLowerCase().includes('redirect') ||
121
+ key.toLowerCase().includes('from') ||
122
+ key.toLowerCase().includes('origin')
123
+ )
124
+ .map(([key, val]) => `${key}=${val}`)
125
+ .join(', '),
126
+ severity: 'info'
127
+ });
128
+ }
129
+
130
+ // Check 4: SPA router state
131
+ if (hasSPARouter && historyLength > 1) {
132
+ indicators.push({
133
+ type: 'SPA router detected',
134
+ historyLength: historyLength,
135
+ state: JSON.stringify(window.history.state)?.slice(0, 100) || '{}',
136
+ severity: 'info'
137
+ });
138
+ }
139
+
140
+ // Check 5: Very fast navigation with minimal content (possible JS redirect)
141
+ const navDuration = navEntry.loadEventEnd - navEntry.startTime;
142
+ const domContentLoaded = navEntry.domContentLoadedEventEnd - navEntry.startTime;
143
+ const responseSize = navEntry.transferSize || navEntry.encodedBodySize || 0;
144
+
145
+ // Only flag if it's BOTH fast AND small (likely a redirect page)
146
+ if (sameOrigin && domContentLoaded < 500 && responseSize < 10000) {
147
+ indicators.push({
148
+ type: 'Fast minimal-content navigation',
149
+ duration: domContentLoaded.toFixed(1) + 'ms',
150
+ size: (responseSize / 1024).toFixed(1) + ' KB',
151
+ note: 'Small page that loads quickly - possible redirect page',
152
+ severity: 'warning'
153
+ });
154
+ }
155
+
156
+ if (indicators.length === 0) {
157
+ console.log('%c āœ… No client-side redirect detected', 'color: #22c55e;');
158
+ } else {
159
+ console.log(
160
+ `%c āš ļø ${indicators.length} indicator(s) found`,
161
+ 'color: #f59e0b;'
162
+ );
163
+ console.log('');
164
+ indicators.forEach((indicator, index) => {
165
+ const icon =
166
+ indicator.severity === 'error' ? 'šŸ”“' :
167
+ indicator.severity === 'warning' ? 'āš ļø' : 'ā„¹ļø';
168
+
169
+ console.log(` ${icon} ${indicator.type}`);
170
+ Object.entries(indicator).forEach(([key, value]) => {
171
+ if (key !== 'type' && key !== 'severity') {
172
+ console.log(` ${key}: ${value}`);
173
+ }
174
+ });
175
+ if (index < indicators.length - 1) console.log('');
176
+ });
177
+ }
178
+
179
+ // LCP Impact Analysis
180
+ if (hasClientRedirect && documentNavigations.length > 0) {
181
+ console.log('');
182
+ console.log('%c⚔ Performance Impact:', 'font-weight: bold;');
183
+
184
+ const totalRedirectOverhead = documentNavigations.reduce(
185
+ (sum, nav) => sum + nav.duration,
186
+ 0
187
+ );
188
+
189
+ console.log(` Total redirect overhead: ${totalRedirectOverhead.toFixed(0)}ms`);
190
+
191
+ if (totalRedirectOverhead > 3000) {
192
+ console.log(
193
+ '%c šŸ”“ CRITICAL: High impact on LCP',
194
+ 'color: #ef4444; font-weight: bold;'
195
+ );
196
+ console.log(' šŸ’” This redirect adds significant delay to page load');
197
+ } else if (totalRedirectOverhead > 1000) {
198
+ console.log(
199
+ '%c 🟔 MODERATE: Noticeable impact on LCP',
200
+ 'color: #f59e0b;'
201
+ );
202
+ } else {
203
+ console.log('%c 🟢 Low impact', 'color: #22c55e;');
204
+ }
205
+ }
206
+
207
+ // Recommendations
208
+ const hasDocumentNavigation = documentNavigations.length > 0;
209
+ const hasSameOriginRedirect = sameOrigin && referrerPath !== currentPath && referrerPath !== '';
210
+ if (hasDocumentNavigation || serverRedirects > 0) {
211
+ console.log('');
212
+ console.log('%cšŸ’” Recommendations:', 'font-weight: bold;');
213
+
214
+ if (serverRedirects > 0) {
215
+ console.log('');
216
+ console.log(' Server-side redirects detected:');
217
+ console.log(' • Minimize redirect chains');
218
+ console.log(' • Use direct links when possible');
219
+ console.log(' • Cache redirect responses with appropriate headers');
220
+ }
221
+
222
+ if (hasDocumentNavigation) {
223
+ console.log('');
224
+ console.log(' Client-side redirect detected:');
225
+ console.log(' • Replace with server-side 301/302 redirects for better performance');
226
+
227
+ if (referrerPath && currentPath) {
228
+ console.log('');
229
+ console.log(' Example Nginx config:');
230
+ console.log(' %clocation = ' + referrerPath + ' {', 'color: #3b82f6; font-family: monospace;');
231
+ console.log(' %c return 301 ' + currentPath + ';', 'color: #3b82f6; font-family: monospace;');
232
+ console.log(' %c}', 'color: #3b82f6; font-family: monospace;');
233
+
234
+ if (referrerPath === '/' && currentPath.match(/^\/[a-z]{2}\//)) {
235
+ console.log('');
236
+ console.log(' Or with language detection:');
237
+ console.log(' %cmap $http_accept_language $lang {', 'color: #3b82f6; font-family: monospace;');
238
+ console.log(' %c default ' + currentPath.split('/')[1] + ';', 'color: #3b82f6; font-family: monospace;');
239
+ console.log(' %c ~*^en en;', 'color: #3b82f6; font-family: monospace;');
240
+ console.log(' %c}', 'color: #3b82f6; font-family: monospace;');
241
+ console.log(' %clocation = / {', 'color: #3b82f6; font-family: monospace;');
242
+ console.log(' %c return 301 /$lang/;', 'color: #3b82f6; font-family: monospace;');
243
+ console.log(' %c}', 'color: #3b82f6; font-family: monospace;');
244
+ }
245
+ }
246
+ } else if (hasSameOriginRedirect) {
247
+ console.log('');
248
+ console.log(' Same-origin navigation detected (no document navigation overhead):');
249
+ console.log(' • This may be SPA routing or a fast redirect');
250
+ console.log(' • If this is intentional SPA behavior, no action needed');
251
+ console.log(' • If this is a redirect, consider server-side 301/302 for consistency');
252
+ }
253
+ }
254
+
255
+ // Detailed timing breakdown
256
+ console.log('');
257
+ console.log('%cā±ļø Navigation Timing:', 'font-weight: bold;');
258
+ console.table({
259
+ 'TTFB': `${(navEntry.responseStart - navEntry.startTime).toFixed(1)}ms`,
260
+ 'DOM Content Loaded': `${domContentLoaded.toFixed(1)}ms`,
261
+ 'Load Complete': `${navDuration.toFixed(1)}ms`,
262
+ 'Redirect Time': serverRedirects > 0 ? `${redirectTime.toFixed(1)}ms` : 'N/A',
263
+ 'DNS Lookup': `${(navEntry.domainLookupEnd - navEntry.domainLookupStart).toFixed(1)}ms`,
264
+ 'TCP Connect': `${(navEntry.connectEnd - navEntry.connectStart).toFixed(1)}ms`,
265
+ 'Request/Response': `${(navEntry.responseEnd - navEntry.requestStart).toFixed(1)}ms`,
266
+ });
267
+
268
+ // Resource timing for document navigations
269
+ if (documentNavigations.length > 0) {
270
+ console.log('');
271
+ console.log(
272
+ '%cšŸ”“ Same-Origin Document Navigations Detected (Potential Client-Side Redirects):',
273
+ 'font-weight: bold; color: #ef4444;'
274
+ );
275
+ console.log('');
276
+ console.log(
277
+ ' These are same-origin navigations that suggest client-side redirects.'
278
+ );
279
+ console.log(
280
+ ' If these are not expected, consider replacing with server-side redirects.'
281
+ );
282
+ console.log('');
283
+ console.table(
284
+ documentNavigations.map((nav) => ({
285
+ 'Type': nav.initiatorType,
286
+ 'Duration (ms)': nav.duration.toFixed(1),
287
+ 'TTFB (ms)': (nav.responseStart - nav.startTime).toFixed(1),
288
+ 'Transfer (KB)': nav.transferSize > 0 ? (nav.transferSize / 1024).toFixed(1) : '0',
289
+ 'URL': nav.name.length > 60 ? '...' + nav.name.slice(-57) : nav.name,
290
+ }))
291
+ );
292
+ }
293
+
294
+ console.log('');
295
+
296
+ if (hasDocumentNavigation) {
297
+ console.log(
298
+ '%cā„¹ļø Note: Document navigations detected indicate actual client-side redirects. ' +
299
+ 'For best performance, replace with server-side 301/302 redirects.',
300
+ 'color: #6b7280; font-style: italic;'
301
+ );
302
+ } else if (hasSameOriginRedirect) {
303
+ console.log(
304
+ '%cā„¹ļø Note: Same-origin navigation detected without document navigation overhead. ' +
305
+ 'This may be SPA routing or browser navigation, which is typically acceptable.',
306
+ 'color: #6b7280; font-style: italic;'
307
+ );
308
+ } else {
309
+ console.log(
310
+ '%cā„¹ļø Note: No client-side redirects detected. ' +
311
+ 'The current page was loaded directly or through server-side redirects.',
312
+ 'color: #6b7280; font-style: italic;'
313
+ );
314
+ }
315
+
316
+ console.groupEnd();
317
+
318
+ return {
319
+ script: "Client-Side-Redirect-Detection",
320
+ status: "ok",
321
+ details: {
322
+ serverRedirects,
323
+ redirectTimeMs: Math.round(redirectTime),
324
+ clientRedirectIndicators: indicators.length,
325
+ documentNavigations: documentNavigations.length,
326
+ hasSPARouter,
327
+ historyLength,
328
+ currentPath,
329
+ referrerPath,
330
+ },
331
+ issues: [
332
+ ...(serverRedirects > 0 ? [{ severity: "warning", message: `${serverRedirects} server-side redirect(s) detected (${Math.round(redirectTime)}ms)` }] : []),
333
+ ...(documentNavigations.length > 0 ? [{ severity: "error", message: `${documentNavigations.length} same-origin document navigation(s) detected — likely client-side redirect` }] : []),
334
+ ...indicators.filter(i => i.severity === "warning").map(i => ({ severity: "warning", message: i.type })),
335
+ ],
336
+ };
337
+ })();
@@ -0,0 +1,352 @@
1
+ // Detect elements with content-visibility: auto and analyze optimization opportunities
2
+
3
+ function detectContentVisibility() {
4
+ // Create an object to store the results
5
+ const results = {
6
+ autoElements: [],
7
+ hiddenElements: [],
8
+ visibleElements: [],
9
+ nodeArray: [],
10
+ };
11
+
12
+ // Get the name of the node
13
+ function getName(node) {
14
+ const name = node.nodeName;
15
+ return node.nodeType === 1 ? name.toLowerCase() : name.toUpperCase().replace(/^#/, "");
16
+ }
17
+
18
+ // Get the selector for an element
19
+ function getSelector(node) {
20
+ let sel = "";
21
+
22
+ try {
23
+ while (node && node.nodeType !== 9) {
24
+ const el = node;
25
+ const part = el.id
26
+ ? "#" + el.id
27
+ : getName(el) +
28
+ (el.classList &&
29
+ el.classList.value &&
30
+ el.classList.value.trim() &&
31
+ el.classList.value.trim().length
32
+ ? "." + el.classList.value.trim().replace(/\s+/g, ".")
33
+ : "");
34
+ if (sel.length + part.length > 100 - 1) return sel || part;
35
+ sel = sel ? part + ">" + sel : part;
36
+ if (el.id) break;
37
+ node = el.parentNode;
38
+ }
39
+ } catch {
40
+ // Do nothing...
41
+ }
42
+ return sel;
43
+ }
44
+
45
+ // Check if element is in viewport
46
+ function isInViewport(element) {
47
+ const rect = element.getBoundingClientRect();
48
+ return (
49
+ rect.top < window.innerHeight &&
50
+ rect.bottom > 0 &&
51
+ rect.left < window.innerWidth &&
52
+ rect.right > 0
53
+ );
54
+ }
55
+
56
+ // Get element dimensions and position
57
+ function getElementInfo(node) {
58
+ const rect = node.getBoundingClientRect();
59
+ const cs = window.getComputedStyle(node);
60
+
61
+ return {
62
+ selector: getSelector(node),
63
+ contentVisibility: cs["content-visibility"],
64
+ containIntrinsicSize: cs["contain-intrinsic-size"] || "not set",
65
+ width: Math.round(rect.width),
66
+ height: Math.round(rect.height),
67
+ top: Math.round(rect.top + window.scrollY),
68
+ inViewport: isInViewport(node),
69
+ };
70
+ }
71
+
72
+ // Recursively find all elements with content-visibility
73
+ function findContentVisibilityElements(node) {
74
+ const cs = window.getComputedStyle(node);
75
+ const cv = cs["content-visibility"];
76
+
77
+ if (cv && cv !== "visible") {
78
+ const info = getElementInfo(node);
79
+
80
+ if (cv === "auto") {
81
+ results.autoElements.push(info);
82
+ results.nodeArray.push(node);
83
+ } else if (cv === "hidden") {
84
+ results.hiddenElements.push(info);
85
+ }
86
+ }
87
+
88
+ for (let i = 0; i < node.children.length; i++) {
89
+ findContentVisibilityElements(node.children[i]);
90
+ }
91
+ }
92
+
93
+ // Run the detection
94
+ findContentVisibilityElements(document.body);
95
+
96
+ // Display results
97
+ console.group("šŸ” Content-Visibility Detection");
98
+
99
+ if (results.autoElements.length === 0 && results.hiddenElements.length === 0) {
100
+ console.log("%cNo content-visibility usage found.", "color: orange; font-weight: bold;");
101
+ console.log("");
102
+ console.log("šŸ’” Consider applying content-visibility: auto to:");
103
+ console.log(" • Footer sections");
104
+ console.log(" • Below-the-fold content");
105
+ console.log(" • Long lists or card grids");
106
+ console.log(" • Tab content that is not initially visible");
107
+ console.log(" • Accordion/collapsible content");
108
+ } else {
109
+ // Auto elements
110
+ if (results.autoElements.length > 0) {
111
+ console.group("āœ… content-visibility: auto");
112
+ console.log(`Found ${results.autoElements.length} element(s)`);
113
+ console.table(results.autoElements);
114
+ console.groupEnd();
115
+ }
116
+
117
+ // Hidden elements
118
+ if (results.hiddenElements.length > 0) {
119
+ console.group("šŸ”’ content-visibility: hidden");
120
+ console.log(`Found ${results.hiddenElements.length} element(s)`);
121
+ console.table(results.hiddenElements);
122
+ console.groupEnd();
123
+ }
124
+
125
+ // Check for missing contain-intrinsic-size
126
+ const missingIntrinsicSize = results.autoElements.filter(
127
+ (el) => el.containIntrinsicSize === "not set" || el.containIntrinsicSize === "none",
128
+ );
129
+
130
+ if (missingIntrinsicSize.length > 0) {
131
+ console.group("āš ļø Missing contain-intrinsic-size");
132
+ console.log(
133
+ "%cThese elements lack contain-intrinsic-size, which may cause layout shifts:",
134
+ "color: #f59e0b; font-weight: bold",
135
+ );
136
+ console.table(
137
+ missingIntrinsicSize.map((el) => ({
138
+ selector: el.selector,
139
+ height: el.height + "px",
140
+ })),
141
+ );
142
+ console.log("");
143
+ console.log("šŸ’” Add contain-intrinsic-size to prevent CLS:");
144
+ console.log(" contain-intrinsic-size: auto 500px;");
145
+ console.groupEnd();
146
+ }
147
+
148
+ // Nodes for inspection
149
+ console.group("šŸ”Ž Elements for inspection");
150
+ console.log("Click to expand and inspect in Elements panel:");
151
+ results.nodeArray.forEach((node, i) => {
152
+ console.log(`${i + 1}. `, node);
153
+ });
154
+ console.groupEnd();
155
+ }
156
+
157
+ console.groupEnd();
158
+
159
+ return results;
160
+ }
161
+
162
+ // Analyze opportunities for content-visibility optimization
163
+ // Options:
164
+ // - threshold: distance from viewport bottom to consider "offscreen" (default: 0)
165
+ // - minHeight: minimum element height in px (default: 100)
166
+ // - minChildren: minimum child elements to be considered (default: 5)
167
+ function analyzeContentVisibilityOpportunities(options = {}) {
168
+ const { threshold = 0, minHeight = 100, minChildren = 5 } = options;
169
+
170
+ const viewportHeight = window.innerHeight;
171
+ const opportunities = [];
172
+ const processedElements = new Set();
173
+
174
+ function getSelector(node) {
175
+ let sel = "";
176
+ try {
177
+ while (node && node.nodeType !== 9) {
178
+ const el = node;
179
+ const name = el.nodeName.toLowerCase();
180
+ const part = el.id
181
+ ? "#" + el.id
182
+ : name +
183
+ (el.classList && el.classList.value && el.classList.value.trim()
184
+ ? "." + el.classList.value.trim().split(/\s+/).slice(0, 2).join(".")
185
+ : "");
186
+ if (sel.length + part.length > 80) return sel || part;
187
+ sel = sel ? part + ">" + sel : part;
188
+ if (el.id) break;
189
+ node = el.parentNode;
190
+ }
191
+ } catch {}
192
+ return sel;
193
+ }
194
+
195
+ function estimateRenderSavings(childCount) {
196
+ const baseMs = childCount * 0.2;
197
+ if (baseMs < 5) return "Low (~" + baseMs.toFixed(1) + "ms)";
198
+ if (baseMs < 20) return "Medium (~" + baseMs.toFixed(1) + "ms)";
199
+ return "High (~" + baseMs.toFixed(1) + "ms)";
200
+ }
201
+
202
+ function isAncestorProcessed(el) {
203
+ let parent = el.parentElement;
204
+ while (parent) {
205
+ if (processedElements.has(parent)) return true;
206
+ parent = parent.parentElement;
207
+ }
208
+ return false;
209
+ }
210
+
211
+ function analyzeElement(el) {
212
+ // Skip already processed or descendant of processed
213
+ if (processedElements.has(el) || isAncestorProcessed(el)) return;
214
+
215
+ const rect = el.getBoundingClientRect();
216
+ const cs = window.getComputedStyle(el);
217
+
218
+ // Skip if already using content-visibility
219
+ if (cs["content-visibility"] && cs["content-visibility"] !== "visible") return;
220
+
221
+ // Skip elements not meeting size criteria
222
+ if (rect.height < minHeight || rect.width === 0) return;
223
+
224
+ // Check if element is below the viewport + threshold
225
+ const distanceFromViewport = rect.top - viewportHeight;
226
+ if (distanceFromViewport < threshold) return;
227
+
228
+ const childCount = el.querySelectorAll("*").length;
229
+
230
+ // Skip elements with too few children
231
+ if (childCount < minChildren) return;
232
+
233
+ processedElements.add(el);
234
+
235
+ opportunities.push({
236
+ selector: getSelector(el),
237
+ height: Math.round(rect.height) + "px",
238
+ distanceFromViewport: Math.round(distanceFromViewport) + "px",
239
+ childElements: childCount,
240
+ estimatedSavings: estimateRenderSavings(childCount),
241
+ element: el,
242
+ });
243
+ }
244
+
245
+ // Walk all elements in the DOM
246
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT, null, false);
247
+
248
+ while (walker.nextNode()) {
249
+ analyzeElement(walker.currentNode);
250
+ }
251
+
252
+ // Sort by child count (highest impact first)
253
+ opportunities.sort((a, b) => b.childElements - a.childElements);
254
+
255
+ // Display results
256
+ console.group("šŸ’” Content-Visibility Opportunities");
257
+ console.log(
258
+ `%cSettings: threshold=${threshold}px, minHeight=${minHeight}px, minChildren=${minChildren}`,
259
+ "color: #888;",
260
+ );
261
+ console.log("");
262
+
263
+ if (opportunities.length === 0) {
264
+ console.log("%cāœ… No opportunities found with current settings.", "color: #22c55e; font-weight: bold");
265
+ console.log("Try adjusting: analyzeContentVisibilityOpportunities({ threshold: -200, minHeight: 50, minChildren: 3 })");
266
+ } else {
267
+ console.log(
268
+ `%cFound ${opportunities.length} element(s) that could benefit from content-visibility: auto`,
269
+ "font-weight: bold;",
270
+ );
271
+ console.log("");
272
+
273
+ // Show table without element reference
274
+ const tableData = opportunities.slice(0, 20).map((opportunity) => ({
275
+ selector: opportunity.selector,
276
+ height: opportunity.height,
277
+ distanceFromViewport: opportunity.distanceFromViewport,
278
+ childElements: opportunity.childElements,
279
+ estimatedSavings: opportunity.estimatedSavings,
280
+ }));
281
+ console.table(tableData);
282
+
283
+ if (opportunities.length > 20) {
284
+ console.log(`... and ${opportunities.length - 20} more elements`);
285
+ }
286
+
287
+ // Log elements for inspection
288
+ console.log("");
289
+ console.group("šŸ”Ž Elements for inspection");
290
+ opportunities.slice(0, 10).forEach((opp, i) => {
291
+ console.log(`${i + 1}. `, opp.element);
292
+ });
293
+ console.groupEnd();
294
+
295
+ console.log("");
296
+ console.group("šŸ“ Implementation Example");
297
+ console.log("Add this CSS to optimize rendering:");
298
+ console.log("");
299
+ console.log(
300
+ "%c/* Optimize offscreen content */\n" +
301
+ ".your-selector {\n" +
302
+ " content-visibility: auto;\n" +
303
+ " contain-intrinsic-size: auto 500px; /* Use actual height */\n" +
304
+ "}",
305
+ "font-family: monospace; background: #1e1e1e; color: #9cdcfe; padding: 10px; border-radius: 4px;",
306
+ );
307
+ console.groupEnd();
308
+ }
309
+
310
+ console.groupEnd();
311
+
312
+ return {
313
+ opportunities: opportunities.map((opportunity) => ({
314
+ selector: opportunity.selector,
315
+ height: opportunity.height,
316
+ distanceFromViewport: opportunity.distanceFromViewport,
317
+ childElements: opportunity.childElements,
318
+ estimatedSavings: opportunity.estimatedSavings,
319
+ })),
320
+ totalElements: opportunities.length,
321
+ highImpact: opportunities.filter((o) => o.estimatedSavings.startsWith("High")).length,
322
+ elements: opportunities.map((o) => o.element),
323
+ };
324
+ }
325
+
326
+ window.analyzeContentVisibilityOpportunities = analyzeContentVisibilityOpportunities;
327
+
328
+ // Run detection
329
+ (() => {
330
+ const cvResults = detectContentVisibility();
331
+ console.log(
332
+ "%c\n To find optimization opportunities, run: %canalyzeContentVisibilityOpportunities()",
333
+ "color: #3b82f6; font-weight: bold;",
334
+ "color: #22c55e; font-weight: bold; font-family: monospace;"
335
+ );
336
+ return {
337
+ script: "Content-Visibility",
338
+ status: "ok",
339
+ count: cvResults.autoElements.length + cvResults.hiddenElements.length,
340
+ details: {
341
+ autoCount: cvResults.autoElements.length,
342
+ hiddenCount: cvResults.hiddenElements.length,
343
+ },
344
+ items: [
345
+ ...cvResults.autoElements.map(el => ({ ...el, type: "auto" })),
346
+ ...cvResults.hiddenElements.map(el => ({ ...el, type: "hidden" })),
347
+ ],
348
+ issues: cvResults.autoElements
349
+ .filter(el => el.containIntrinsicSize === "not set" || el.containIntrinsicSize === "none")
350
+ .map(el => ({ severity: "warning", message: `${el.selector}: missing contain-intrinsic-size (CLS risk)` })),
351
+ };
352
+ })();