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,473 @@
1
+ // Back/Forward Cache (bfcache) Analysis
2
+ // https://webperf-snippets.nucliweb.net
3
+
4
+ (() => {
5
+ const results = {
6
+ supported: 'PerformanceNavigationTiming' in window,
7
+ wasRestored: false,
8
+ eligibility: null,
9
+ blockingReasons: [],
10
+ recommendations: [],
11
+ };
12
+
13
+ // Check if current page was restored from bfcache
14
+ const checkRestoration = () => {
15
+ // Check via pageshow event
16
+ window.addEventListener('pageshow', (event) => {
17
+ if (event.persisted) {
18
+ results.wasRestored = true;
19
+ console.log(
20
+ '%c⚡ Page restored from bfcache!',
21
+ 'color: #22c55e; font-weight: bold; font-size: 14px;'
22
+ );
23
+ console.log(' Navigation was instant (0ms)');
24
+ }
25
+ });
26
+
27
+ // Check via Performance Navigation Timing
28
+ if (results.supported) {
29
+ const navEntry = performance.getEntriesByType('navigation')[0];
30
+ if (navEntry && navEntry.type === 'back_forward') {
31
+ // Check activation timing for bfcache restore
32
+ if (navEntry.activationStart > 0) {
33
+ results.wasRestored = true;
34
+ }
35
+ }
36
+ }
37
+ };
38
+
39
+ // Test bfcache eligibility
40
+ const testEligibility = () => {
41
+ const issues = [];
42
+ const recs = [];
43
+
44
+ // 1. Check for unload handlers
45
+ const hasUnload = window.onunload !== null || window.onbeforeunload !== null;
46
+ if (hasUnload) {
47
+ issues.push({
48
+ reason: 'unload/beforeunload handler detected',
49
+ severity: 'high',
50
+ description: 'These handlers block bfcache',
51
+ });
52
+ recs.push('Remove unload/beforeunload handlers. Use pagehide or visibilitychange instead.');
53
+ }
54
+
55
+ // 2. Check for Cache-Control: no-store
56
+ // Note: Can't check response headers from JS, but we can detect it indirectly
57
+ const meta = document.querySelector('meta[http-equiv="Cache-Control"]');
58
+ if (meta && meta.content.includes('no-store')) {
59
+ issues.push({
60
+ reason: 'Cache-Control: no-store in meta tag',
61
+ severity: 'high',
62
+ description: 'Prevents page from being cached',
63
+ });
64
+ recs.push('Remove Cache-Control: no-store or change to no-cache.');
65
+ }
66
+
67
+ // 3. Check for open IndexedDB connections
68
+ if (window.indexedDB) {
69
+ // Can't directly check open connections, but we can warn
70
+ const hasIndexedDB = performance.getEntriesByType('resource').some(
71
+ r => r.name.includes('indexedDB')
72
+ );
73
+ if (hasIndexedDB) {
74
+ issues.push({
75
+ reason: 'IndexedDB may be in use',
76
+ severity: 'medium',
77
+ description: 'Open IndexedDB transactions block bfcache',
78
+ });
79
+ recs.push('Close IndexedDB connections before page hide.');
80
+ }
81
+ }
82
+
83
+ // 4. Check for broadcast channels
84
+ // Can't directly detect, but can check if BroadcastChannel exists
85
+ if (window.BroadcastChannel) {
86
+ // Just a warning - we can't detect if actually in use
87
+ issues.push({
88
+ reason: 'BroadcastChannel API available (check if in use)',
89
+ severity: 'low',
90
+ description: 'Open BroadcastChannel connections may block bfcache',
91
+ });
92
+ }
93
+
94
+ // 5. Check for embedded iframes with issues
95
+ const iframes = document.querySelectorAll('iframe');
96
+ if (iframes.length > 0) {
97
+ issues.push({
98
+ reason: `${iframes.length} iframe(s) detected`,
99
+ severity: 'medium',
100
+ description: 'Iframes with bfcache blockers will block parent page',
101
+ });
102
+ recs.push('Ensure iframes are also bfcache compatible.');
103
+ }
104
+
105
+ // 6. Check for Service Worker
106
+ if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
107
+ // Service workers are OK, but just noting it
108
+ issues.push({
109
+ reason: 'Service Worker active',
110
+ severity: 'info',
111
+ description: 'Service Workers with fetch handlers are generally OK, but check for ongoing operations',
112
+ });
113
+ }
114
+
115
+ // 7. Check for open WebSocket/WebRTC
116
+ // Can't directly detect, but we can check for common libraries
117
+ if (window.WebSocket) {
118
+ issues.push({
119
+ reason: 'WebSocket API available (check if connections are open)',
120
+ severity: 'medium',
121
+ description: 'Open WebSocket connections block bfcache',
122
+ });
123
+ recs.push('Close WebSocket connections before page hide.');
124
+ }
125
+
126
+ // 8. Check for ongoing fetch/XHR
127
+ // Check if there are active resource requests
128
+ const resources = performance.getEntriesByType('resource');
129
+ const recent = resources.filter(r => r.responseEnd === 0 || (performance.now() - r.responseEnd < 100));
130
+ if (recent.length > 0) {
131
+ issues.push({
132
+ reason: `${recent.length} recent/ongoing network requests`,
133
+ severity: 'low',
134
+ description: 'Ongoing requests may prevent bfcache',
135
+ });
136
+ recs.push('Ensure requests complete or are aborted on page hide.');
137
+ }
138
+
139
+ results.blockingReasons = issues;
140
+ results.recommendations = recs;
141
+
142
+ // Determine eligibility
143
+ const highSeverity = issues.filter(i => i.severity === 'high').length;
144
+ const mediumSeverity = issues.filter(i => i.severity === 'medium').length;
145
+
146
+ if (highSeverity > 0) {
147
+ results.eligibility = 'blocked';
148
+ } else if (mediumSeverity > 1) {
149
+ results.eligibility = 'likely-blocked';
150
+ } else if (issues.length > 0) {
151
+ results.eligibility = 'potentially-eligible';
152
+ } else {
153
+ results.eligibility = 'likely-eligible';
154
+ }
155
+
156
+ return results.eligibility;
157
+ };
158
+
159
+ // Display results
160
+ const displayResults = () => {
161
+ const statusIcons = {
162
+ 'likely-eligible': '🟢',
163
+ 'potentially-eligible': '🟡',
164
+ 'likely-blocked': '🟠',
165
+ 'blocked': '🔴',
166
+ };
167
+
168
+ const statusColors = {
169
+ 'likely-eligible': '#22c55e',
170
+ 'potentially-eligible': '#f59e0b',
171
+ 'likely-blocked': '#fb923c',
172
+ 'blocked': '#ef4444',
173
+ };
174
+
175
+ const statusText = {
176
+ 'likely-eligible': 'Likely Eligible',
177
+ 'potentially-eligible': 'Potentially Eligible',
178
+ 'likely-blocked': 'Likely Blocked',
179
+ 'blocked': 'Blocked',
180
+ };
181
+
182
+ const icon = statusIcons[results.eligibility] || '⚪';
183
+ const color = statusColors[results.eligibility] || '#6b7280';
184
+ const text = statusText[results.eligibility] || 'Unknown';
185
+
186
+ console.group(
187
+ `%c${icon} bfcache Status: ${text}`,
188
+ `color: ${color}; font-weight: bold; font-size: 14px;`
189
+ );
190
+
191
+ console.log('');
192
+ console.log('%c📊 Status:', 'font-weight: bold;');
193
+
194
+ if (results.wasRestored) {
195
+ console.log('%c ✅ This page WAS restored from bfcache', 'color: #22c55e;');
196
+ console.log(' Navigation was instant!');
197
+ } else {
198
+ console.log(' ℹ️ This page was NOT restored from bfcache');
199
+ console.log(' (Either first visit or bfcache was blocked on previous navigation)');
200
+ }
201
+
202
+ // Navigation timing comparison
203
+ if (results.supported) {
204
+ const navEntry = performance.getEntriesByType('navigation')[0];
205
+ if (navEntry) {
206
+ console.log('');
207
+ console.log('%c🕐 Navigation Timing:', 'font-weight: bold;');
208
+ console.log(` Type: ${navEntry.type}`);
209
+ console.log(` Duration: ${Math.round(navEntry.duration)}ms`);
210
+
211
+ if (navEntry.type === 'back_forward') {
212
+ if (navEntry.duration < 10) {
213
+ console.log('%c ⚡ Fast back/forward (likely from bfcache)', 'color: #22c55e;');
214
+ } else {
215
+ console.log('%c 🐌 Slow back/forward (full reload)', 'color: #ef4444;');
216
+ }
217
+ }
218
+ }
219
+ }
220
+
221
+ // Display issues
222
+ if (results.blockingReasons.length > 0) {
223
+ console.log('');
224
+ console.log('%c🔍 Potential Issues:', 'font-weight: bold;');
225
+
226
+ const issueTable = results.blockingReasons.map(issue => ({
227
+ Severity: issue.severity.toUpperCase(),
228
+ Issue: issue.reason,
229
+ Impact: issue.description,
230
+ }));
231
+
232
+ console.table(issueTable);
233
+ } else {
234
+ console.log('');
235
+ console.log('%c✅ No obvious bfcache blockers detected!', 'color: #22c55e; font-weight: bold;');
236
+ }
237
+
238
+ // Recommendations
239
+ if (results.recommendations.length > 0) {
240
+ console.log('');
241
+ console.log('%c💡 Recommendations:', 'color: #3b82f6; font-weight: bold;');
242
+ results.recommendations.forEach((rec, idx) => {
243
+ console.log(` ${idx + 1}. ${rec}`);
244
+ });
245
+ }
246
+
247
+ // How to test
248
+ console.log('');
249
+ console.log('%c🧪 How to Test:', 'font-weight: bold;');
250
+ console.log('%c IMPORTANT: Run snippet BEFORE navigating away!', 'color: #f59e0b;');
251
+ console.log('');
252
+ console.log(' 1. Run this snippet (you already did this ✓)');
253
+ console.log(' 2. Navigate to another page');
254
+ console.log(' 3. Click browser Back button');
255
+ console.log(' 4. Check console for restoration message');
256
+ console.log('');
257
+ console.log(' Or use Chrome DevTools → Application → Back/forward cache');
258
+
259
+ console.groupEnd();
260
+
261
+ return results;
262
+ };
263
+
264
+ // Check for NotRestoredReasons API (Chrome 123+)
265
+ const checkNotRestoredReasons = () => {
266
+ if (!('PerformanceNavigationTiming' in window)) {
267
+ return null;
268
+ }
269
+
270
+ const navEntry = performance.getEntriesByType('navigation')[0];
271
+ if (!navEntry || !navEntry.notRestoredReasons) {
272
+ return null;
273
+ }
274
+
275
+ console.group('%c🔬 Not Restored Reasons (Chrome 123+)', 'font-weight: bold; color: #3b82f6;');
276
+
277
+ const reasons = navEntry.notRestoredReasons;
278
+
279
+ console.log('');
280
+ console.log('%cPage-level information:', 'font-weight: bold;');
281
+
282
+ if (reasons.blocked === true) {
283
+ console.log('%c ❌ Page was blocked from bfcache', 'color: #ef4444;');
284
+ } else {
285
+ console.log('%c ✅ Page was not blocked', 'color: #22c55e;');
286
+ }
287
+
288
+ if (reasons.url) {
289
+ console.log(` URL: ${reasons.url}`);
290
+ }
291
+
292
+ if (reasons.id) {
293
+ console.log(` Frame ID: ${reasons.id}`);
294
+ }
295
+
296
+ if (reasons.name) {
297
+ console.log(` Frame name: ${reasons.name}`);
298
+ }
299
+
300
+ if (reasons.src) {
301
+ console.log(` Source: ${reasons.src}`);
302
+ }
303
+
304
+ // Parse NotRestoredReasonDetails array
305
+ // Each entry is an object with { reason: string, source: string }
306
+ // Common reasons: "WebSocket", "unload-listener", "IndexedDB", etc.
307
+ // Sources: "JavaScript", "UserAgentOnly", etc.
308
+ if (reasons.reasons && reasons.reasons.length > 0) {
309
+ console.log('');
310
+ console.log('%cBlocking reasons:', 'font-weight: bold; color: #ef4444;');
311
+
312
+ reasons.reasons.forEach((reasonDetail, idx) => {
313
+ // reasonDetail is a NotRestoredReasonDetails object
314
+ const reasonName = reasonDetail.reason || 'Unknown reason';
315
+ const reasonSource = reasonDetail.source || 'Unknown source';
316
+
317
+ console.group(` ${idx + 1}. ${reasonName}`);
318
+
319
+ // Show detailed information
320
+ console.log(` Reason: ${reasonName}`);
321
+ console.log(` Source: ${reasonSource}`);
322
+
323
+ // Add helpful context for common reasons
324
+ const reasonExplanations = {
325
+ 'WebSocket': 'Open WebSocket connections prevent bfcache. Close them on pagehide event.',
326
+ 'unload-listener': 'unload event listeners block bfcache. Use pagehide or visibilitychange instead.',
327
+ 'response-cache-control-no-store': 'Cache-Control: no-store header prevents caching. Change to no-cache.',
328
+ 'IndexedDB': 'Open IndexedDB transactions block bfcache. Close connections on pagehide.',
329
+ 'BroadcastChannel': 'Open BroadcastChannel prevents bfcache. Close it on pagehide.',
330
+ 'dedicated-worker': 'Dedicated workers can block bfcache. Terminate them on pagehide.',
331
+ };
332
+
333
+ if (reasonExplanations[reasonName]) {
334
+ console.log(` 💡 ${reasonExplanations[reasonName]}`);
335
+ }
336
+
337
+ console.groupEnd();
338
+ });
339
+ }
340
+
341
+ // Check children (iframes)
342
+ if (reasons.children && reasons.children.length > 0) {
343
+ console.log('');
344
+ console.log('%cEmbedded frames:', 'font-weight: bold;');
345
+
346
+ reasons.children.forEach((child, idx) => {
347
+ console.group(` ${idx + 1}. ${child.url || child.src || 'iframe'}`);
348
+
349
+ if (child.blocked) {
350
+ console.log('%c Status: BLOCKED', 'color: #ef4444;');
351
+ } else {
352
+ console.log('%c Status: OK', 'color: #22c55e;');
353
+ }
354
+
355
+ if (child.id) {
356
+ console.log(` Frame ID: ${child.id}`);
357
+ }
358
+
359
+ if (child.name) {
360
+ console.log(` Frame name: ${child.name}`);
361
+ }
362
+
363
+ if (child.reasons && child.reasons.length > 0) {
364
+ console.log(' Blocking reasons:');
365
+ child.reasons.forEach(reasonDetail => {
366
+ console.log(` • ${reasonDetail.reason || 'Unknown'}`);
367
+ if (reasonDetail.source) {
368
+ console.log(` Source: ${reasonDetail.source}`);
369
+ }
370
+ });
371
+ }
372
+
373
+ console.groupEnd();
374
+ });
375
+ }
376
+
377
+ // Summary table for easier visualization
378
+ if (reasons.reasons && reasons.reasons.length > 0) {
379
+ console.log('');
380
+ console.log('%c📋 Summary Table:', 'font-weight: bold;');
381
+
382
+ const reasonsTable = reasons.reasons.map(r => ({
383
+ Reason: r.reason || 'Unknown',
384
+ Source: r.source || 'N/A',
385
+ }));
386
+
387
+ console.table(reasonsTable);
388
+ }
389
+
390
+ console.groupEnd();
391
+ return reasons;
392
+ };
393
+
394
+ // Check if snippet was executed after a back/forward navigation
395
+ const checkExecutionTiming = () => {
396
+ const navEntry = performance.getEntriesByType('navigation')[0];
397
+ if (navEntry && navEntry.type === 'back_forward') {
398
+ console.log('');
399
+ console.group('%c⚠️ Timing Warning', 'color: #f59e0b; font-weight: bold;');
400
+ console.log('%cSnippet executed AFTER back/forward navigation.', 'color: #f59e0b;');
401
+ console.log('');
402
+ console.log('For complete analysis including bfcache restoration detection:');
403
+ console.log(' 1. Run this snippet FIRST');
404
+ console.log(' 2. Then navigate away');
405
+ console.log(' 3. Then click Back button');
406
+ console.log('');
407
+ console.log('Current analysis shows NotRestoredReasons from the navigation that just occurred.');
408
+ console.groupEnd();
409
+ console.log('');
410
+ }
411
+ };
412
+
413
+ // Initialize
414
+ checkRestoration();
415
+ testEligibility();
416
+
417
+ // Display after a short delay to ensure pageshow event fires
418
+ setTimeout(() => {
419
+ checkExecutionTiming();
420
+ displayResults();
421
+
422
+ // Check NotRestoredReasons API if available
423
+ const notRestoredReasons = checkNotRestoredReasons();
424
+ if (!notRestoredReasons) {
425
+ console.log('');
426
+ console.log('%cℹ️ For detailed reasons, use Chrome 123+ and navigate back to this page.', 'color: #6b7280;');
427
+ }
428
+ }, 100);
429
+
430
+ // Expose function for manual check
431
+ window.checkBfcache = () => {
432
+ testEligibility();
433
+ displayResults();
434
+ checkNotRestoredReasons();
435
+ return {
436
+ script: "Back-Forward-Cache",
437
+ status: "ok",
438
+ details: {
439
+ eligibility: results.eligibility,
440
+ wasRestored: results.wasRestored,
441
+ supported: results.supported,
442
+ },
443
+ issues: results.blockingReasons.map(i => ({
444
+ severity: i.severity === 'high' ? 'error' : i.severity === 'medium' ? 'warning' : 'info',
445
+ message: i.reason,
446
+ })),
447
+ };
448
+ };
449
+
450
+ console.log('%c🚀 bfcache Analysis Running...', 'font-weight: bold; font-size: 14px;');
451
+ console.log(' Results will appear shortly.');
452
+ console.log(
453
+ ' Call %ccheckBfcache()%c anytime to re-run analysis.',
454
+ 'font-family: monospace; background: #f3f4f6; padding: 2px 4px;',
455
+ ''
456
+ );
457
+
458
+ return {
459
+ script: "Back-Forward-Cache",
460
+ status: "ok",
461
+ details: {
462
+ eligibility: results.eligibility,
463
+ wasRestored: results.wasRestored,
464
+ supported: results.supported,
465
+ },
466
+ issues: results.blockingReasons.map(i => ({
467
+ severity: i.severity === 'high' ? 'error' : i.severity === 'medium' ? 'warning' : 'info',
468
+ message: i.reason,
469
+ })),
470
+ message: "bfcache analysis running. Call checkBfcache() to re-run analysis.",
471
+ getDataFn: "checkBfcache",
472
+ };
473
+ })();