webperf-snippets 0.2.0 → 0.3.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 (65) hide show
  1. package/README.md +120 -5
  2. package/package.json +1 -1
  3. package/snippets/CoreWebVitals/CLS.js +29 -15
  4. package/snippets/CoreWebVitals/INP.js +14 -7
  5. package/snippets/CoreWebVitals/LCP-Image-Entropy.js +29 -11
  6. package/snippets/CoreWebVitals/LCP-Subparts.js +4 -0
  7. package/snippets/CoreWebVitals/LCP-Trail.js +14 -3
  8. package/snippets/CoreWebVitals/LCP-Video-Candidate.js +20 -2
  9. package/snippets/CoreWebVitals/LCP.js +4 -2
  10. package/snippets/DevTools-Overrides/Fetch-XHR-Timeline-read.js +9 -2
  11. package/snippets/Interaction/DOM-Size-and-Depth.js +134 -0
  12. package/snippets/Interaction/Forced-Synchronous-Layout.js +33 -7
  13. package/snippets/Interaction/Input-Latency-Breakdown.js +45 -1
  14. package/snippets/Interaction/Interactions.js +16 -5
  15. package/snippets/Interaction/Layout-Shift-Loading-and-Interaction.js +44 -13
  16. package/snippets/Interaction/Long-Animation-Frames-Helpers.js +28 -1
  17. package/snippets/Interaction/Long-Animation-Frames-Script-Attribution.js +8 -0
  18. package/snippets/Interaction/Long-Animation-Frames.js +31 -2
  19. package/snippets/Interaction/LongTask.js +37 -5
  20. package/snippets/Interaction/Scroll-Performance.js +40 -0
  21. package/snippets/Interaction/WORKFLOWS.md +9 -0
  22. package/snippets/Loading/Back-Forward-Cache.js +174 -381
  23. package/snippets/Loading/CSS-Media-Queries-Analysis.js +76 -44
  24. package/snippets/Loading/Cache-Strategy-Analysis.js +52 -30
  25. package/snippets/Loading/Compression-Audit.js +172 -0
  26. package/snippets/Loading/Content-Visibility.js +8 -3
  27. package/snippets/Loading/Critical-CSS-Detection.js +8 -6
  28. package/snippets/Loading/Event-Processing-Time.js +15 -2
  29. package/snippets/Loading/FCP.js +39 -5
  30. package/snippets/Loading/Find-Above-The-Fold-Lazy-Loaded-Images.js +1 -1
  31. package/snippets/Loading/Find-non-Lazy-Loaded-Images-outside-of-the-viewport.js +7 -5
  32. package/snippets/Loading/First-And-Third-Party-Script-Info.js +40 -20
  33. package/snippets/Loading/First-And-Third-Party-Script-Timings.js +21 -13
  34. package/snippets/Loading/Fonts-Preloaded-Loaded-and-used-above-the-fold.js +35 -4
  35. package/snippets/Loading/Inline-CSS-Info-and-Size.js +8 -6
  36. package/snippets/Loading/Inline-Script-Info-and-Size.js +14 -7
  37. package/snippets/Loading/JS-Execution-Time-Breakdown.js +23 -10
  38. package/snippets/Loading/Priority-Hints-Audit.js +4 -1
  39. package/snippets/Loading/Resource-Hints-Validation.js +15 -0
  40. package/snippets/Loading/Resource-Hints.js +29 -8
  41. package/snippets/Loading/SSR-Hydration-Data-Analysis.js +77 -8
  42. package/snippets/Loading/Script-Loading.js +55 -25
  43. package/snippets/Loading/Server-Timing-Early-Hints.js +126 -0
  44. package/snippets/Loading/Service-Worker-Analysis.js +229 -53
  45. package/snippets/Loading/Speculation-Rules-Inspector.js +134 -0
  46. package/snippets/Loading/TTFB-Resources.js +59 -18
  47. package/snippets/Loading/TTFB-Sub-Parts.js +9 -2
  48. package/snippets/Loading/TTFB.js +3 -2
  49. package/snippets/Loading/Third-Party-Impact-by-Domain.js +216 -0
  50. package/snippets/Loading/Validate-Preload-Async-Defer-Scripts.js +21 -3
  51. package/snippets/Loading/WORKFLOWS.md +47 -1
  52. package/snippets/Loading/Webfont-Usage-Analyzer.js +332 -0
  53. package/snippets/Media/Image-Element-Audit.js +22 -3
  54. package/snippets/Media/Oversized-Images.js +217 -0
  55. package/snippets/Media/SVG-Embedded-Bitmap-Analysis.js +18 -3
  56. package/snippets/Media/WORKFLOWS.md +22 -0
  57. package/snippets/Resources/Network-Bandwidth-Connection-Quality.js +7 -0
  58. package/snippets/SCHEMA.md +1176 -0
  59. package/snippets/_shared/helpers.js +44 -0
  60. package/src/bin.js +55 -3
  61. package/src/crux.js +84 -0
  62. package/src/report-to.js +46 -0
  63. package/src/reporters/human.js +20 -1
  64. package/src/reporters/markdown.js +22 -1
  65. package/src/runner.js +4 -2
@@ -1,473 +1,266 @@
1
1
  // Back/Forward Cache (bfcache) Analysis
2
2
  // https://webperf-snippets.nucliweb.net
3
3
 
4
- (() => {
4
+ (async () => {
5
+ // Only what the browser can report is analysed. Several blockers cannot be detected from
6
+ // JavaScript (unload listeners added with addEventListener, open WebSocket, BroadcastChannel
7
+ // or IndexedDB connections). After a back navigation, the NotRestoredReasons API reports
8
+ // the blockers the browser found, including those of embedded frames.
5
9
  const results = {
6
10
  supported: 'PerformanceNavigationTiming' in window,
7
11
  wasRestored: false,
8
12
  eligibility: null,
9
13
  blockingReasons: [],
14
+ notRestoredReasons: null,
10
15
  recommendations: [],
11
16
  };
12
17
 
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
- }
18
+ // A pageshow event with persisted = true is the only reliable sign of a bfcache restore
19
+ window.addEventListener('pageshow', (event) => {
20
+ if (event.persisted) {
21
+ results.wasRestored = true;
22
+ console.log(
23
+ '%c⚡ Page restored from bfcache!',
24
+ 'color: #22c55e; font-weight: bold; font-size: 14px;'
25
+ );
36
26
  }
27
+ });
28
+
29
+ const REASON_HELP = {
30
+ 'unload-listener': 'unload event listeners block bfcache. Use pagehide or visibilitychange instead.',
31
+ 'response-cache-control-no-store': 'Cache-Control: no-store on the page response prevents caching. Use no-cache instead.',
32
+ 'websocket': 'Open WebSocket connections prevent bfcache. Close them on pagehide.',
33
+ 'broadcastchannel': 'Open BroadcastChannel instances prevent bfcache. Close them on pagehide.',
34
+ 'indexeddb-connection': 'Open IndexedDB connections prevent bfcache. Close them on pagehide.',
35
+ 'masked': 'The browser does not disclose the exact reason (for example, a cross-origin frame).',
37
36
  };
38
37
 
39
- // Test bfcache eligibility
40
- const testEligibility = () => {
38
+ const serializeReasons = (node) =>
39
+ node && {
40
+ url: node.url ?? null,
41
+ src: node.src ?? null,
42
+ id: node.id ?? null,
43
+ name: node.name ?? null,
44
+ // Each entry is a NotRestoredReasonDetails object with a reason property
45
+ // (plain strings are accepted as well)
46
+ reasons: (node.reasons || []).map((r) => (typeof r === 'string' ? r : r?.reason)).filter(Boolean),
47
+ children: (node.children || []).map(serializeReasons),
48
+ };
49
+
50
+ const flattenReasons = (node, frame) => [
51
+ ...node.reasons.map((reason) => ({ reason, frame })),
52
+ ...node.children.flatMap((child, i) =>
53
+ flattenReasons(child, child.src || child.url || child.name || child.id || `iframe ${i + 1}`)
54
+ ),
55
+ ];
56
+
57
+ const analyze = async () => {
41
58
  const issues = [];
42
- const recs = [];
59
+ const navEntry = performance.getEntriesByType('navigation')[0];
43
60
 
44
- // 1. Check for unload handlers
45
- const hasUnload = window.onunload !== null || window.onbeforeunload !== null;
46
- if (hasUnload) {
61
+ // 1. Blockers reported by the browser (after a back/forward navigation)
62
+ results.notRestoredReasons = serializeReasons(navEntry?.notRestoredReasons);
63
+ const confirmed = results.notRestoredReasons
64
+ ? flattenReasons(results.notRestoredReasons, results.notRestoredReasons.url || 'main frame')
65
+ : [];
66
+ confirmed.forEach(({ reason, frame }) => {
47
67
  issues.push({
48
- reason: 'unload/beforeunload handler detected',
68
+ reason: `${reason} (${frame})`,
49
69
  severity: 'high',
50
- description: 'These handlers block bfcache',
70
+ source: 'browser',
71
+ description: REASON_HELP[reason.toLowerCase()] || 'Reported by the browser as a bfcache blocker',
51
72
  });
52
- recs.push('Remove unload/beforeunload handlers. Use pagehide or visibilitychange instead.');
53
- }
73
+ });
54
74
 
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')) {
75
+ // 2. unload handler assigned as a property. Listeners added with addEventListener
76
+ // cannot be detected from JavaScript.
77
+ if (window.onunload !== null && !confirmed.some(({ reason }) => reason === 'unload-listener')) {
59
78
  issues.push({
60
- reason: 'Cache-Control: no-store in meta tag',
79
+ reason: 'window.onunload handler set',
61
80
  severity: 'high',
62
- description: 'Prevents page from being cached',
81
+ source: 'detected',
82
+ description: REASON_HELP['unload-listener'],
63
83
  });
64
- recs.push('Remove Cache-Control: no-store or change to no-cache.');
65
84
  }
66
85
 
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.');
86
+ // 3. Cache-Control: no-store on the page response. A HEAD request to the current URL
87
+ // exposes the header; the value can differ from the original navigation.
88
+ if (/^https?:$/.test(location.protocol)) {
89
+ try {
90
+ const res = await fetch(location.href, { method: 'HEAD', credentials: 'same-origin' });
91
+ const cacheControl = res.headers.get('cache-control') || '';
92
+ if (/no-store/i.test(cacheControl) && !confirmed.some(({ reason }) => reason === 'response-cache-control-no-store')) {
93
+ issues.push({
94
+ reason: 'Cache-Control: no-store on the page response',
95
+ severity: 'medium',
96
+ source: 'detected',
97
+ description: REASON_HELP['response-cache-control-no-store'],
98
+ });
99
+ }
100
+ } catch {
101
+ // The header could not be read; nothing is reported
80
102
  }
81
103
  }
82
104
 
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) {
105
+ // 4. Context that is worth knowing but is not a blocker by itself
106
+ const iframeCount = document.querySelectorAll('iframe').length;
107
+ if (iframeCount > 0) {
97
108
  issues.push({
98
- reason: `${iframes.length} iframe(s) detected`,
99
- severity: 'medium',
100
- description: 'Iframes with bfcache blockers will block parent page',
109
+ reason: `${iframeCount} iframe(s) on the page`,
110
+ severity: 'info',
111
+ source: 'note',
112
+ description: 'A bfcache blocker inside an iframe blocks the whole page. See notRestoredReasons after a back navigation.',
101
113
  });
102
- recs.push('Ensure iframes are also bfcache compatible.');
103
114
  }
104
115
 
105
- // 6. Check for Service Worker
106
116
  if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
107
- // Service workers are OK, but just noting it
108
117
  issues.push({
109
- reason: 'Service Worker active',
118
+ reason: 'Service Worker controls the page',
110
119
  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',
120
+ source: 'note',
121
+ description: 'Service Workers are compatible with bfcache.',
135
122
  });
136
- recs.push('Ensure requests complete or are aborted on page hide.');
137
123
  }
138
124
 
139
125
  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) {
126
+ results.recommendations = [
127
+ ...new Set(
128
+ issues
129
+ .filter((i) => i.severity !== 'info')
130
+ .map((i) => i.description)
131
+ ),
132
+ ];
133
+
134
+ if (issues.some((i) => i.source === 'browser')) {
147
135
  results.eligibility = 'blocked';
148
- } else if (mediumSeverity > 1) {
136
+ } else if (issues.some((i) => i.source === 'detected')) {
149
137
  results.eligibility = 'likely-blocked';
150
- } else if (issues.length > 0) {
151
- results.eligibility = 'potentially-eligible';
152
138
  } else {
153
- results.eligibility = 'likely-eligible';
139
+ results.eligibility = 'no-blockers-detected';
154
140
  }
155
141
 
156
142
  return results.eligibility;
157
143
  };
158
144
 
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
- };
145
+ const printReasons = (node, indent) => {
146
+ const pad = ' '.repeat(indent);
147
+ node.reasons.forEach((reason) => {
148
+ console.log(`${pad}• ${reason}`);
149
+ const help = REASON_HELP[reason.toLowerCase()];
150
+ if (help) console.log(`${pad} 💡 ${help}`);
151
+ });
152
+ node.children.forEach((child, i) => {
153
+ console.log(`${pad}Frame: ${child.src || child.url || child.name || child.id || `iframe ${i + 1}`}`);
154
+ printReasons(child, indent + 3);
155
+ });
156
+ };
174
157
 
158
+ const displayResults = () => {
159
+ const statusIcons = { 'no-blockers-detected': '🟢', 'likely-blocked': '🟠', blocked: '🔴' };
160
+ const statusColors = { 'no-blockers-detected': '#22c55e', 'likely-blocked': '#fb923c', blocked: '#ef4444' };
175
161
  const statusText = {
176
- 'likely-eligible': 'Likely Eligible',
177
- 'potentially-eligible': 'Potentially Eligible',
178
- 'likely-blocked': 'Likely Blocked',
179
- 'blocked': 'Blocked',
162
+ 'no-blockers-detected': 'No blockers detected',
163
+ 'likely-blocked': 'Likely blocked',
164
+ blocked: 'Blocked (reported by the browser)',
180
165
  };
181
166
 
182
- const icon = statusIcons[results.eligibility] || '⚪';
183
- const color = statusColors[results.eligibility] || '#6b7280';
184
- const text = statusText[results.eligibility] || 'Unknown';
185
-
186
167
  console.group(
187
- `%c${icon} bfcache Status: ${text}`,
188
- `color: ${color}; font-weight: bold; font-size: 14px;`
168
+ `%c${statusIcons[results.eligibility] || '⚪'} bfcache: ${statusText[results.eligibility] || 'Unknown'}`,
169
+ `color: ${statusColors[results.eligibility] || '#6b7280'}; font-weight: bold; font-size: 14px;`
189
170
  );
190
171
 
172
+ const navEntry = performance.getEntriesByType('navigation')[0];
191
173
  console.log('');
192
174
  console.log('%c📊 Status:', 'font-weight: bold;');
193
-
194
175
  if (results.wasRestored) {
195
- console.log('%c ✅ This page WAS restored from bfcache', 'color: #22c55e;');
196
- console.log(' Navigation was instant!');
176
+ console.log('%c ✅ This page was restored from bfcache', 'color: #22c55e;');
177
+ } else if (navEntry?.type === 'back_forward') {
178
+ console.log(' ℹ️ This page was loaded by a back/forward navigation, not restored from bfcache');
197
179
  } 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
- }
180
+ console.log(' ℹ️ This page was loaded normally. Restoration is reported by a pageshow event.');
219
181
  }
182
+ if (navEntry) console.log(` Navigation type: ${navEntry.type}`);
220
183
 
221
- // Display issues
222
184
  if (results.blockingReasons.length > 0) {
223
185
  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);
186
+ console.log('%c🔍 Findings:', 'font-weight: bold;');
187
+ console.table(
188
+ results.blockingReasons.map((i) => ({
189
+ Severity: i.severity.toUpperCase(),
190
+ Finding: i.reason,
191
+ Detail: i.description,
192
+ }))
193
+ );
233
194
  } else {
234
195
  console.log('');
235
- console.log('%c✅ No obvious bfcache blockers detected!', 'color: #22c55e; font-weight: bold;');
196
+ console.log('%c✅ No bfcache blockers detected', 'color: #22c55e; font-weight: bold;');
236
197
  }
237
198
 
238
- // Recommendations
239
- if (results.recommendations.length > 0) {
199
+ if (results.notRestoredReasons) {
240
200
  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;
201
+ console.group('%c🔬 NotRestoredReasons (reported by the browser)', 'font-weight: bold; color: #3b82f6;');
202
+ if (results.notRestoredReasons.reasons.length === 0 && results.notRestoredReasons.children.length === 0) {
203
+ console.log(' No reasons reported');
204
+ }
205
+ printReasons(results.notRestoredReasons, 3);
206
+ console.groupEnd();
273
207
  }
274
208
 
275
- console.group('%c🔬 Not Restored Reasons (Chrome 123+)', 'font-weight: bold; color: #3b82f6;');
276
-
277
- const reasons = navEntry.notRestoredReasons;
278
-
279
209
  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
- }
210
+ console.log(
211
+ '%cNot every blocker is detectable from JavaScript: unload listeners added with addEventListener and open WebSocket, BroadcastChannel or IndexedDB connections only show up in NotRestoredReasons after a back navigation, or in DevTools → Application → Back/forward cache.',
212
+ 'color: #6b7280;'
213
+ );
340
214
 
341
- // Check children (iframes)
342
- if (reasons.children && reasons.children.length > 0) {
215
+ if (results.recommendations.length > 0) {
343
216
  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
- });
217
+ console.log('%c💡 Recommendations:', 'color: #3b82f6; font-weight: bold;');
218
+ results.recommendations.forEach((rec, idx) => console.log(` ${idx + 1}. ${rec}`));
375
219
  }
376
220
 
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
- }
221
+ console.log('');
222
+ console.log('%c🧪 How to test:', 'font-weight: bold;');
223
+ console.log(' 1. Navigate to another page');
224
+ console.log(' 2. Click the browser Back button');
225
+ console.log(' 3. Run this snippet again to read NotRestoredReasons, or use DevTools → Application → Back/forward cache');
389
226
 
390
227
  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
228
  };
412
229
 
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);
230
+ const buildResult = () => ({
231
+ script: 'Back-Forward-Cache',
232
+ status: 'ok',
233
+ details: {
234
+ eligibility: results.eligibility,
235
+ wasRestored: results.wasRestored,
236
+ supported: results.supported,
237
+ navigationType: performance.getEntriesByType('navigation')[0]?.type ?? null,
238
+ notRestoredReasons: results.notRestoredReasons,
239
+ },
240
+ issues: results.blockingReasons.map((i) => ({
241
+ severity: i.severity === 'high' ? 'error' : i.severity === 'medium' ? 'warning' : 'info',
242
+ message: i.reason,
243
+ })),
244
+ });
429
245
 
430
246
  // Expose function for manual check
431
- window.checkBfcache = () => {
432
- testEligibility();
247
+ window.checkBfcache = async () => {
248
+ await analyze();
433
249
  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
- };
250
+ return buildResult();
448
251
  };
449
252
 
450
- console.log('%c🚀 bfcache Analysis Running...', 'font-weight: bold; font-size: 14px;');
451
- console.log(' Results will appear shortly.');
253
+ console.log('%c🚀 bfcache Analysis', 'font-weight: bold; font-size: 14px;');
452
254
  console.log(
453
255
  ' Call %ccheckBfcache()%c anytime to re-run analysis.',
454
256
  'font-family: monospace; background: #f3f4f6; padding: 2px 4px;',
455
257
  ''
456
258
  );
457
259
 
260
+ await analyze();
261
+ displayResults();
458
262
  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",
263
+ ...buildResult(),
264
+ message: 'bfcache analysis complete. Call checkBfcache() to re-run analysis.',
472
265
  };
473
266
  })();