Back/Forward Cache (bfcache)
Overview
Analyzes Back/Forward Cache (bfcache) (opens in a new tab) to determine if your page is eligible for instant back/forward navigations. When bfcache works, users get instant (0ms) page loads when using browser back/forward buttons, dramatically improving perceived performance.
Why this matters:
bfcache is one of the most impactful performance optimizations available - it can make navigation instantaneous. However, many pages are unknowingly ineligible due to common patterns like unload handlers, Cache-Control: no-store, or ongoing network requests. This snippet helps identify why your page isn't being cached and how to fix it.
bfcache Impact:
| Navigation Type | Load Time | User Experience |
|---|---|---|
| Normal navigation | 1-3s | Wait, spinner |
| bfcache restoration | 0ms | Instant |
How bfcache works:
Common blocking reasons:
Snippet
// Back/Forward Cache (bfcache) Analysis
// https://webperf-snippets.nucliweb.net
(() => {
const results = {
supported: 'PerformanceNavigationTiming' in window,
wasRestored: false,
eligibility: null,
blockingReasons: [],
recommendations: [],
};
// Check if current page was restored from bfcache
const checkRestoration = () => {
// Check via pageshow event
window.addEventListener('pageshow', (event) => {
if (event.persisted) {
results.wasRestored = true;
console.log(
'%c⚡ Page restored from bfcache!',
'color: #22c55e; font-weight: bold; font-size: 14px;'
);
console.log(' Navigation was instant (0ms)');
}
});
// Check via Performance Navigation Timing
if (results.supported) {
const navEntry = performance.getEntriesByType('navigation')[0];
if (navEntry && navEntry.type === 'back_forward') {
// Check activation timing for bfcache restore
if (navEntry.activationStart > 0) {
results.wasRestored = true;
}
}
}
};
// Test bfcache eligibility
const testEligibility = () => {
const issues = [];
const recs = [];
// 1. Check for unload handlers
const hasUnload = window.onunload !== null || window.onbeforeunload !== null;
if (hasUnload) {
issues.push({
reason: 'unload/beforeunload handler detected',
severity: 'high',
description: 'These handlers block bfcache',
});
recs.push('Remove unload/beforeunload handlers. Use pagehide or visibilitychange instead.');
}
// 2. Check for Cache-Control: no-store
// Note: Can't check response headers from JS, but we can detect it indirectly
const meta = document.querySelector('meta[http-equiv="Cache-Control"]');
if (meta && meta.content.includes('no-store')) {
issues.push({
reason: 'Cache-Control: no-store in meta tag',
severity: 'high',
description: 'Prevents page from being cached',
});
recs.push('Remove Cache-Control: no-store or change to no-cache.');
}
// 3. Check for open IndexedDB connections
if (window.indexedDB) {
// Can't directly check open connections, but we can warn
const hasIndexedDB = performance.getEntriesByType('resource').some(
r => r.name.includes('indexedDB')
);
if (hasIndexedDB) {
issues.push({
reason: 'IndexedDB may be in use',
severity: 'medium',
description: 'Open IndexedDB transactions block bfcache',
});
recs.push('Close IndexedDB connections before page hide.');
}
}
// 4. Check for broadcast channels
// Can't directly detect, but can check if BroadcastChannel exists
if (window.BroadcastChannel) {
// Just a warning - we can't detect if actually in use
issues.push({
reason: 'BroadcastChannel API available (check if in use)',
severity: 'low',
description: 'Open BroadcastChannel connections may block bfcache',
});
}
// 5. Check for embedded iframes with issues
const iframes = document.querySelectorAll('iframe');
if (iframes.length > 0) {
issues.push({
reason: `${iframes.length} iframe(s) detected`,
severity: 'medium',
description: 'Iframes with bfcache blockers will block parent page',
});
recs.push('Ensure iframes are also bfcache compatible.');
}
// 6. Check for Service Worker
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
// Service workers are OK, but just noting it
issues.push({
reason: 'Service Worker active',
severity: 'info',
description: 'Service Workers with fetch handlers are generally OK, but check for ongoing operations',
});
}
// 7. Check for open WebSocket/WebRTC
// Can't directly detect, but we can check for common libraries
if (window.WebSocket) {
issues.push({
reason: 'WebSocket API available (check if connections are open)',
severity: 'medium',
description: 'Open WebSocket connections block bfcache',
});
recs.push('Close WebSocket connections before page hide.');
}
// 8. Check for ongoing fetch/XHR
// Check if there are active resource requests
const resources = performance.getEntriesByType('resource');
const recent = resources.filter(r => r.responseEnd === 0 || (performance.now() - r.responseEnd < 100));
if (recent.length > 0) {
issues.push({
reason: `${recent.length} recent/ongoing network requests`,
severity: 'low',
description: 'Ongoing requests may prevent bfcache',
});
recs.push('Ensure requests complete or are aborted on page hide.');
}
results.blockingReasons = issues;
results.recommendations = recs;
// Determine eligibility
const highSeverity = issues.filter(i => i.severity === 'high').length;
const mediumSeverity = issues.filter(i => i.severity === 'medium').length;
if (highSeverity > 0) {
results.eligibility = 'blocked';
} else if (mediumSeverity > 1) {
results.eligibility = 'likely-blocked';
} else if (issues.length > 0) {
results.eligibility = 'potentially-eligible';
} else {
results.eligibility = 'likely-eligible';
}
return results.eligibility;
};
// Display results
const displayResults = () => {
const statusIcons = {
'likely-eligible': '🟢',
'potentially-eligible': '🟡',
'likely-blocked': '🟠',
'blocked': '🔴',
};
const statusColors = {
'likely-eligible': '#22c55e',
'potentially-eligible': '#f59e0b',
'likely-blocked': '#fb923c',
'blocked': '#ef4444',
};
const statusText = {
'likely-eligible': 'Likely Eligible',
'potentially-eligible': 'Potentially Eligible',
'likely-blocked': 'Likely Blocked',
'blocked': 'Blocked',
};
const icon = statusIcons[results.eligibility] || '⚪';
const color = statusColors[results.eligibility] || '#6b7280';
const text = statusText[results.eligibility] || 'Unknown';
console.group(
`%c${icon} bfcache Status: ${text}`,
`color: ${color}; font-weight: bold; font-size: 14px;`
);
console.log('');
console.log('%c📊 Status:', 'font-weight: bold;');
if (results.wasRestored) {
console.log('%c ✅ This page WAS restored from bfcache', 'color: #22c55e;');
console.log(' Navigation was instant!');
} else {
console.log(' ℹ️ This page was NOT restored from bfcache');
console.log(' (Either first visit or bfcache was blocked on previous navigation)');
}
// Navigation timing comparison
if (results.supported) {
const navEntry = performance.getEntriesByType('navigation')[0];
if (navEntry) {
console.log('');
console.log('%c🕐 Navigation Timing:', 'font-weight: bold;');
console.log(` Type: ${navEntry.type}`);
console.log(` Duration: ${Math.round(navEntry.duration)}ms`);
if (navEntry.type === 'back_forward') {
if (navEntry.duration < 10) {
console.log('%c ⚡ Fast back/forward (likely from bfcache)', 'color: #22c55e;');
} else {
console.log('%c 🐌 Slow back/forward (full reload)', 'color: #ef4444;');
}
}
}
}
// Display issues
if (results.blockingReasons.length > 0) {
console.log('');
console.log('%c🔍 Potential Issues:', 'font-weight: bold;');
const issueTable = results.blockingReasons.map(issue => ({
Severity: issue.severity.toUpperCase(),
Issue: issue.reason,
Impact: issue.description,
}));
console.table(issueTable);
} else {
console.log('');
console.log('%c✅ No obvious bfcache blockers detected!', 'color: #22c55e; font-weight: bold;');
}
// Recommendations
if (results.recommendations.length > 0) {
console.log('');
console.log('%c💡 Recommendations:', 'color: #3b82f6; font-weight: bold;');
results.recommendations.forEach((rec, idx) => {
console.log(` ${idx + 1}. ${rec}`);
});
}
// How to test
console.log('');
console.log('%c🧪 How to Test:', 'font-weight: bold;');
console.log('%c IMPORTANT: Run snippet BEFORE navigating away!', 'color: #f59e0b;');
console.log('');
console.log(' 1. Run this snippet (you already did this ✓)');
console.log(' 2. Navigate to another page');
console.log(' 3. Click browser Back button');
console.log(' 4. Check console for restoration message');
console.log('');
console.log(' Or use Chrome DevTools → Application → Back/forward cache');
console.groupEnd();
return results;
};
// Check for NotRestoredReasons API (Chrome 123+)
const checkNotRestoredReasons = () => {
if (!('PerformanceNavigationTiming' in window)) {
return null;
}
const navEntry = performance.getEntriesByType('navigation')[0];
if (!navEntry || !navEntry.notRestoredReasons) {
return null;
}
console.group('%c🔬 Not Restored Reasons (Chrome 123+)', 'font-weight: bold; color: #3b82f6;');
const reasons = navEntry.notRestoredReasons;
console.log('');
console.log('%cPage-level information:', 'font-weight: bold;');
if (reasons.blocked === true) {
console.log('%c ❌ Page was blocked from bfcache', 'color: #ef4444;');
} else {
console.log('%c ✅ Page was not blocked', 'color: #22c55e;');
}
if (reasons.url) {
console.log(` URL: ${reasons.url}`);
}
if (reasons.id) {
console.log(` Frame ID: ${reasons.id}`);
}
if (reasons.name) {
console.log(` Frame name: ${reasons.name}`);
}
if (reasons.src) {
console.log(` Source: ${reasons.src}`);
}
// Parse NotRestoredReasonDetails array
// Each entry is an object with { reason: string, source: string }
// Common reasons: "WebSocket", "unload-listener", "IndexedDB", etc.
// Sources: "JavaScript", "UserAgentOnly", etc.
if (reasons.reasons && reasons.reasons.length > 0) {
console.log('');
console.log('%cBlocking reasons:', 'font-weight: bold; color: #ef4444;');
reasons.reasons.forEach((reasonDetail, idx) => {
// reasonDetail is a NotRestoredReasonDetails object
const reasonName = reasonDetail.reason || 'Unknown reason';
const reasonSource = reasonDetail.source || 'Unknown source';
console.group(` ${idx + 1}. ${reasonName}`);
// Show detailed information
console.log(` Reason: ${reasonName}`);
console.log(` Source: ${reasonSource}`);
// Add helpful context for common reasons
const reasonExplanations = {
'WebSocket': 'Open WebSocket connections prevent bfcache. Close them on pagehide event.',
'unload-listener': 'unload event listeners block bfcache. Use pagehide or visibilitychange instead.',
'response-cache-control-no-store': 'Cache-Control: no-store header prevents caching. Change to no-cache.',
'IndexedDB': 'Open IndexedDB transactions block bfcache. Close connections on pagehide.',
'BroadcastChannel': 'Open BroadcastChannel prevents bfcache. Close it on pagehide.',
'dedicated-worker': 'Dedicated workers can block bfcache. Terminate them on pagehide.',
};
if (reasonExplanations[reasonName]) {
console.log(` 💡 ${reasonExplanations[reasonName]}`);
}
console.groupEnd();
});
}
// Check children (iframes)
if (reasons.children && reasons.children.length > 0) {
console.log('');
console.log('%cEmbedded frames:', 'font-weight: bold;');
reasons.children.forEach((child, idx) => {
console.group(` ${idx + 1}. ${child.url || child.src || 'iframe'}`);
if (child.blocked) {
console.log('%c Status: BLOCKED', 'color: #ef4444;');
} else {
console.log('%c Status: OK', 'color: #22c55e;');
}
if (child.id) {
console.log(` Frame ID: ${child.id}`);
}
if (child.name) {
console.log(` Frame name: ${child.name}`);
}
if (child.reasons && child.reasons.length > 0) {
console.log(' Blocking reasons:');
child.reasons.forEach(reasonDetail => {
console.log(` • ${reasonDetail.reason || 'Unknown'}`);
if (reasonDetail.source) {
console.log(` Source: ${reasonDetail.source}`);
}
});
}
console.groupEnd();
});
}
// Summary table for easier visualization
if (reasons.reasons && reasons.reasons.length > 0) {
console.log('');
console.log('%c📋 Summary Table:', 'font-weight: bold;');
const reasonsTable = reasons.reasons.map(r => ({
Reason: r.reason || 'Unknown',
Source: r.source || 'N/A',
}));
console.table(reasonsTable);
}
console.groupEnd();
return reasons;
};
// Check if snippet was executed after a back/forward navigation
const checkExecutionTiming = () => {
const navEntry = performance.getEntriesByType('navigation')[0];
if (navEntry && navEntry.type === 'back_forward') {
console.log('');
console.group('%c⚠️ Timing Warning', 'color: #f59e0b; font-weight: bold;');
console.log('%cSnippet executed AFTER back/forward navigation.', 'color: #f59e0b;');
console.log('');
console.log('For complete analysis including bfcache restoration detection:');
console.log(' 1. Run this snippet FIRST');
console.log(' 2. Then navigate away');
console.log(' 3. Then click Back button');
console.log('');
console.log('Current analysis shows NotRestoredReasons from the navigation that just occurred.');
console.groupEnd();
console.log('');
}
};
// Initialize
checkRestoration();
const eligibility = testEligibility();
// Display after a short delay to ensure pageshow event fires
setTimeout(() => {
checkExecutionTiming();
displayResults();
// Check NotRestoredReasons API if available
const notRestoredReasons = checkNotRestoredReasons();
if (!notRestoredReasons) {
console.log('');
console.log('%cℹ️ For detailed reasons, use Chrome 123+ and navigate back to this page.', 'color: #6b7280;');
}
}, 100);
// Expose function for manual check
window.checkBfcache = () => {
testEligibility();
displayResults();
checkNotRestoredReasons();
return results;
};
console.log('%c🚀 bfcache Analysis Running...', 'font-weight: bold; font-size: 14px;');
console.log(' Results will appear shortly.');
console.log(
' Call %ccheckBfcache()%c anytime to re-run analysis.',
'font-family: monospace; background: #f3f4f6; padding: 2px 4px;',
''
);
})();Understanding bfcache
What is bfcache:
The back/forward cache (bfcache) stores a complete snapshot of a page when you navigate away. When you press the browser back button, the page is restored instantly from memory instead of reloading.
Benefits:
- 0ms navigation: Instant page restoration
- Preserved scroll position: Users return to exact scroll position
- Preserved form state: Form inputs maintain their values
- Preserved JavaScript state: Variables, timers, everything intact
When bfcache works:
User flow with bfcache:
Page A → Page B → [Back] → Page A (instant, 0ms)
User flow without bfcache:
Page A → Page B → [Back] → Page A (full reload, 1-3s)Common Blockers and Solutions
| Blocker | Why it blocks | Solution |
|---|---|---|
| unload event | Can't be reliably replayed | Use pagehide or visibilitychange |
| beforeunload | Prevents navigation caching | Remove or use pagehide for cleanup |
| Cache-Control: no-store | Tells browser not to cache | Change to no-cache or private |
| Open IndexedDB transaction | State can't be serialized | Close transactions on pagehide |
| WebSocket connection | Can't be suspended | Close on pagehide, reconnect on pageshow |
| Ongoing fetch/XHR | Incomplete network state | Abort on pagehide |
| BroadcastChannel | Can't be suspended | Close on pagehide |
Fixing Common Issues
Issue 1: unload/beforeunload handlers
Bad:
window.addEventListener('unload', () => {
saveData(); // Blocks bfcache
});
window.addEventListener('beforeunload', () => {
return 'Are you sure?'; // Blocks bfcache
});Good:
window.addEventListener('pagehide', (event) => {
// event.persisted tells you if page goes to bfcache
if (event.persisted) {
// Page will be cached, do minimal cleanup
console.log('Page going to bfcache');
} else {
// Page being discarded, do full cleanup
saveData();
}
});Issue 2: Cache-Control: no-store
Bad:
<meta http-equiv="Cache-Control" content="no-store">Good:
<!-- Remove the meta tag entirely, or use: -->
<meta http-equiv="Cache-Control" content="no-cache">Issue 3: Open WebSocket
Bad:
const ws = new WebSocket('wss://example.com');
// Connection stays open, blocks bfcacheGood:
const ws = new WebSocket('wss://example.com');
window.addEventListener('pagehide', () => {
ws.close(); // Close on navigation
});
window.addEventListener('pageshow', (event) => {
if (event.persisted) {
// Reconnect after bfcache restore
ws = new WebSocket('wss://example.com');
}
});Issue 4: IndexedDB
Bad:
const request = indexedDB.open('myDB');
request.onsuccess = () => {
const db = request.result;
const transaction = db.transaction(['store'], 'readwrite');
// Transaction stays open, blocks bfcache
};Good:
let db;
const request = indexedDB.open('myDB');
request.onsuccess = () => {
db = request.result;
};
window.addEventListener('pagehide', () => {
db?.close(); // Close connection
});Testing bfcache
Method 1: Manual test (recommended)
⚠️ Important: Execution order matters!
Correct order:
- First: Open your page
- Second: Run this snippet (to register event listeners)
- Third: Navigate to another page (e.g., click a link)
- Fourth: Press browser Back button
- Fifth: Check console for results
Why this order? The snippet needs to be listening for the pageshow event with persisted: true to detect bfcache restoration. If you run the snippet after clicking Back, you'll miss the restoration event.
If you run the snippet after navigating back:
- ⚠️ You'll see a timing warning
- ❌ Won't detect if page was restored from bfcache
- ✅ Will still show
NotRestoredReasonsfrom that navigation - ✅ Will still analyze current eligibility
Output when run in correct order:
⚡ Page restored from bfcache!
Navigation was instant (0ms)Method 2: Chrome DevTools
- Open DevTools → Application tab
- Click "Back/forward cache" in sidebar
- Click "Test back/forward cache"
- DevTools will navigate forward then back
- See detailed blocking reasons
Method 3: Chrome flags (for testing)
Visit chrome://flags/#back-forward-cache and enable experimental features for testing.
NotRestoredReasons API (Chrome 123+)
Chrome 123+ provides the detailed notRestoredReasons API that shows exactly why bfcache was blocked. This is the most accurate way to diagnose bfcache issues.
API structure:
const navEntry = performance.getEntriesByType('navigation')[0];
if (navEntry.notRestoredReasons) {
const reasons = navEntry.notRestoredReasons;
// Page-level information
console.log('Blocked:', reasons.blocked); // boolean
console.log('URL:', reasons.url); // page URL
console.log('Frame ID:', reasons.id); // frame identifier
console.log('Name:', reasons.name); // frame name (if any)
console.log('Source:', reasons.src); // frame source (if iframe)
// Array of NotRestoredReasonDetails objects
reasons.reasons.forEach(detail => {
console.log('Reason:', detail.reason); // blocking reason
console.log('Source:', detail.source); // where it came from
});
// Nested iframes
reasons.children.forEach(child => {
console.log('Child URL:', child.url);
console.log('Child blocked:', child.blocked);
console.log('Child reasons:', child.reasons);
});
}The snippet automatically parses and displays this information in a readable format with color-coding and tables.
Understanding NotRestoredReasonDetails
Each blocking reason is a NotRestoredReasonDetails object with these properties:
| Property | Type | Description |
|---|---|---|
reason | string | The specific blocking reason (e.g., "WebSocket", "unload-listener") |
source | string | Source location: "UserAgentOnly" (browser), "JavaScript" (your code), etc. |
Common reason values:
| Reason | What it means |
|---|---|
unload-listener | Page has unload event listener |
response-cache-control | Cache-Control: no-store header |
WebSocket | Open WebSocket connection |
IndexedDB | Open IndexedDB transaction |
BroadcastChannel | Open BroadcastChannel |
related-active-contents | Related page (popup/iframe) blocks caching |
Example output interpretation:
// Real example from joanleon.dev
{
blocked: false,
url: "https://joanleon.dev/",
reasons: [
{
reason: "WebSocket",
source: "JavaScript"
}
]
}This tells us:
- ✅ Page was not blocked this time (blocked: false)
- ❌ But a WebSocket connection was detected
- 📍 It came from JavaScript code (not browser extension)
- 🎯 Action: Close WebSocket on
pagehideevent to enable bfcache
Interpreting Your Results
Scenario 1: Fast back/forward (< 10ms)
Type: back_forward
Duration: 8ms
✅ Page restored from bfcache✅ Excellent! bfcache is working perfectly.
Scenario 2: Slow back/forward (> 100ms) with reasons
Type: back_forward
Duration: 405ms
🐌 Slow back/forward (full reload)
Blocking reasons:
- WebSocket connection detected
- Source: JavaScript❌ Action needed: Your page has blockers preventing bfcache. Fix the listed issues.
Scenario 3: Blocked = false, but slow navigation
blocked: false
Duration: 405ms
Reasons: [WebSocket]⚠️ Important: Even though blocked: false, the page wasn't cached on the previous navigation because of WebSocket. Fix it and test again with a fresh navigation cycle.
Testing Workflow
To get accurate notRestoredReasons data:
-
First navigation: Open page → Navigate away
- At this point, browser decides if page goes to bfcache
- If there are blockers, page is discarded
-
Back navigation: Click back button
notRestoredReasonsis populated based on previous navigation- Check if
blocked: trueand what the reasons were
-
Fix issues: Close connections, remove handlers
-
Test again: Repeat from step 1
- The previous test's blockers are now stored in
notRestoredReasons - Run the snippet after clicking back to see results
- The previous test's blockers are now stored in
Chrome DevTools shortcut:
- DevTools → Application → Back/forward cache
- Click "Test back/forward cache" button
- Instantly see detailed blocking reasons
Reading the Output
The snippet displays detailed information about NotRestoredReasonDetails objects:
Blocking reasons format:
Blocking reasons:
1. WebSocket
Reason: WebSocket
Source: JavaScript
💡 Open WebSocket connections prevent bfcache. Close them on pagehide event.
📋 Summary Table:
┌─────────┬────────────────┬────────────┐
│ (index) │ Reason │ Source │
├─────────┼────────────────┼────────────┤
│ 0 │ 'WebSocket' │'JavaScript'│
└─────────┴────────────────┴────────────┘Each blocking reason shows:
- Reason: The specific blocker (e.g., "WebSocket", "unload-listener")
- Source: Where it comes from ("JavaScript", "UserAgentOnly")
- Context: Helpful explanation and recommended fix
- Table: Summary of all reasons for easy comparison
Browser Support
| Browser | bfcache Support | NotRestoredReasons API |
|---|---|---|
| Chrome 96+ | ✅ | ✅ (123+) |
| Edge 96+ | ✅ | ✅ (123+) |
| Safari 14+ | ✅ | ❌ |
| Firefox 86+ | ✅ | ❌ |
All modern browsers support bfcache, but diagnostics vary.
Common Mistakes and Real-World Examples
❌ Mistake 1: Running Snippet After Navigation (Wrong Order)
Real example from joanleon.dev when executed incorrectly:
What happened:
- User visited https://joanleon.dev/ (opens in a new tab)
- Clicked on a blog post
- Clicked browser Back button ← Navigation already happened
- Then ran the snippet ← Too late!
Output received:
⚠️ Timing Warning
Snippet executed AFTER back/forward navigation.
For complete analysis including bfcache restoration detection:
1. Run this snippet FIRST
2. Then navigate away
3. Then click Back button
🟡 bfcache Status: Potentially Eligible
📊 Status:
ℹ️ This page was NOT restored from bfcache
(Either first visit or bfcache was blocked on previous navigation)
🕐 Navigation Timing:
Type: back_forward
Duration: 405ms
🐌 Slow back/forward (full reload)
🔍 Potential Issues:
• LOW: BroadcastChannel API available
• MEDIUM: WebSocket API available (check if connections are open)
🔬 Not Restored Reasons (Chrome 123+):
Page-level information:
✅ Page was not blocked
URL: https://joanleon.dev/
Blocking reasons:
1. WebSocket
Reason: WebSocket
Source: JavaScript
💡 Open WebSocket connections prevent bfcache. Close them on pagehide event.Why this output is incomplete:
- ❌ Missed the
pageshowevent withpersisted=true - ❌ Cannot confirm if page was actually restored from bfcache
- ✅ Still shows
NotRestoredReasons(helpful for diagnosis) - ✅ Still shows current eligibility analysis
Diagnosis from available data:
- ✅ Good news: No unload handlers, no Cache-Control issues
- ❌ Problem: WebSocket connection blocking bfcache
- 📊 Impact: 405ms reload vs. potential 0ms with bfcache
- 🎯 Solution needed: Close WebSocket on
pagehide
✅ Example 2: Running Snippet in Correct Order
Correct workflow for joanleon.dev:
What to do:
- Visit https://joanleon.dev/ (opens in a new tab)
- First: Run the snippet ← Sets up event listeners
- Click on a blog post
- Click browser Back button ← Snippet captures everything
Output with listeners active (before fix):
🚀 bfcache Analysis Running...
Listening for pageshow event...
Ready to detect bfcache restoration.
[User navigates away and comes back...]
🟡 bfcache Status: Potentially Eligible
📊 Status:
ℹ️ This page was NOT restored from bfcache
(Snippet detected bfcache was blocked on this navigation)
🕐 Navigation Timing:
Type: back_forward
Duration: 405ms
🐌 Slow back/forward (full reload)
🔍 Potential Issues:
• MEDIUM: WebSocket API available (check if connections are open)
🔬 Not Restored Reasons (Chrome 123+):
Blocking reasons:
1. WebSocket
Reason: WebSocket
Source: JavaScript
💡 Open WebSocket connections prevent bfcache. Close them on pagehide event.Fix applied:
let ws = new WebSocket('wss://example.com/updates');
// Close connection when leaving page
window.addEventListener('pagehide', () => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.close();
}
});
// Reconnect after bfcache restore
window.addEventListener('pageshow', (event) => {
if (event.persisted) {
// Page restored from bfcache, reconnect
ws = new WebSocket('wss://example.com/updates');
}
});Output after fix (with snippet running first):
🚀 bfcache Analysis Running...
Listening for pageshow event...
[User navigates away and comes back...]
⚡ Page restored from bfcache!
Navigation was instant (0ms)
🟢 bfcache Status: Likely Eligible
📊 Status:
✅ This page WAS restored from bfcache
Navigation was instant!
🕐 Navigation Timing:
Type: back_forward
Duration: 3ms
⚡ Fast back/forward (likely from bfcache)
🔍 Potential Issues:
• No significant blockers detected
🔬 Not Restored Reasons (Chrome 123+):
Page-level information:
✅ Page was not blocked
URL: https://joanleon.dev/Impact: 405ms → 3ms = 99.3% faster back navigations
Key takeaway: Running the snippet in the correct order provides complete visibility into bfcache restoration, allowing you to confirm that fixes actually work.
Real-World Impact
Case studies:
- Wikipedia: 40% faster back navigations with bfcache
- E-commerce sites: 50% reduction in bounce rate from back button
- News sites: 60% of back navigations became instant
Measuring impact:
Track navigation timing with performance.getEntriesByType('navigation'):
- With bfcache: duration < 10ms, type === 'back_forward'
- Without bfcache: duration > 1000ms, full reload
RUM Integration
// Track bfcache restoration in RUM
window.addEventListener('pageshow', (event) => {
if (event.persisted) {
// Page restored from bfcache
gtag('event', 'bfcache_restoration', {
navigation_type: 'back_forward',
duration: 0,
});
}
});
// Track bfcache eligibility
const navEntry = performance.getEntriesByType('navigation')[0];
if (navEntry && navEntry.notRestoredReasons) {
const blocked = navEntry.notRestoredReasons.blocked;
const reasons = navEntry.notRestoredReasons.reasons || [];
gtag('event', 'bfcache_eligibility', {
blocked: blocked,
reasons: reasons.join(', '),
});
}Further Reading
- Back/forward cache (bfcache) (opens in a new tab) | web.dev
- NotRestoredReasons API (opens in a new tab) | Chrome Developers
- Page Lifecycle API (opens in a new tab) | Chrome Developers
- bfcache tester (opens in a new tab) | Interactive testing tool