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,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
|
+
})();
|