Back/forward cache (bfcache)
Chrome 57+ Edge 12+ Firefox 58+ Safari 15+ Details
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 open connections. This snippet reports the blockers it can detect and reads the NotRestoredReasons the browser reports after a back navigation, including those of embedded frames.
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
(async () => {
// Only what the browser can report is analysed. Several blockers cannot be detected from
// JavaScript (unload listeners added with addEventListener, open WebSocket, BroadcastChannel
// or IndexedDB connections). After a back navigation, the NotRestoredReasons API reports
// the blockers the browser found, including those of embedded frames.
const results = {
supported: 'PerformanceNavigationTiming' in window,
wasRestored: false,
eligibility: null,
blockingReasons: [],
notRestoredReasons: null,
recommendations: [],
};
// A pageshow event with persisted = true is the only reliable sign of a bfcache restore
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;'
);
}
});
const REASON_HELP = {
'unload-listener': 'unload event listeners block bfcache. Use pagehide or visibilitychange instead.',
'response-cache-control-no-store': 'Cache-Control: no-store on the page response prevents caching. Use no-cache instead.',
'websocket': 'Open WebSocket connections prevent bfcache. Close them on pagehide.',
'broadcastchannel': 'Open BroadcastChannel instances prevent bfcache. Close them on pagehide.',
'indexeddb-connection': 'Open IndexedDB connections prevent bfcache. Close them on pagehide.',
'masked': 'The browser does not disclose the exact reason (for example, a cross-origin frame).',
};
const serializeReasons = (node) =>
node && {
url: node.url ?? null,
src: node.src ?? null,
id: node.id ?? null,
name: node.name ?? null,
// Each entry is a NotRestoredReasonDetails object with a reason property
// (plain strings are accepted as well)
reasons: (node.reasons || []).map((r) => (typeof r === 'string' ? r : r?.reason)).filter(Boolean),
children: (node.children || []).map(serializeReasons),
};
const flattenReasons = (node, frame) => [
...node.reasons.map((reason) => ({ reason, frame })),
...node.children.flatMap((child, i) =>
flattenReasons(child, child.src || child.url || child.name || child.id || `iframe ${i + 1}`)
),
];
const analyze = async () => {
const issues = [];
const navEntry = performance.getEntriesByType('navigation')[0];
// 1. Blockers reported by the browser (after a back/forward navigation)
results.notRestoredReasons = serializeReasons(navEntry?.notRestoredReasons);
const confirmed = results.notRestoredReasons
? flattenReasons(results.notRestoredReasons, results.notRestoredReasons.url || 'main frame')
: [];
confirmed.forEach(({ reason, frame }) => {
issues.push({
reason: `${reason} (${frame})`,
severity: 'high',
source: 'browser',
description: REASON_HELP[reason.toLowerCase()] || 'Reported by the browser as a bfcache blocker',
});
});
// 2. unload handler assigned as a property. Listeners added with addEventListener
// cannot be detected from JavaScript.
if (window.onunload !== null && !confirmed.some(({ reason }) => reason === 'unload-listener')) {
issues.push({
reason: 'window.onunload handler set',
severity: 'high',
source: 'detected',
description: REASON_HELP['unload-listener'],
});
}
// 3. Cache-Control: no-store on the page response. A HEAD request to the current URL
// exposes the header; the value can differ from the original navigation.
if (/^https?:$/.test(location.protocol)) {
try {
const res = await fetch(location.href, { method: 'HEAD', credentials: 'same-origin' });
const cacheControl = res.headers.get('cache-control') || '';
if (/no-store/i.test(cacheControl) && !confirmed.some(({ reason }) => reason === 'response-cache-control-no-store')) {
issues.push({
reason: 'Cache-Control: no-store on the page response',
severity: 'medium',
source: 'detected',
description: REASON_HELP['response-cache-control-no-store'],
});
}
} catch {
// The header could not be read; nothing is reported
}
}
// 4. Context that is worth knowing but is not a blocker by itself
const iframeCount = document.querySelectorAll('iframe').length;
if (iframeCount > 0) {
issues.push({
reason: `${iframeCount} iframe(s) on the page`,
severity: 'info',
source: 'note',
description: 'A bfcache blocker inside an iframe blocks the whole page. See notRestoredReasons after a back navigation.',
});
}
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
issues.push({
reason: 'Service Worker controls the page',
severity: 'info',
source: 'note',
description: 'Service Workers are compatible with bfcache.',
});
}
results.blockingReasons = issues;
results.recommendations = [
...new Set(
issues
.filter((i) => i.severity !== 'info')
.map((i) => i.description)
),
];
if (issues.some((i) => i.source === 'browser')) {
results.eligibility = 'blocked';
} else if (issues.some((i) => i.source === 'detected')) {
results.eligibility = 'likely-blocked';
} else {
results.eligibility = 'no-blockers-detected';
}
return results.eligibility;
};
const printReasons = (node, indent) => {
const pad = ' '.repeat(indent);
node.reasons.forEach((reason) => {
console.log(`${pad}• ${reason}`);
const help = REASON_HELP[reason.toLowerCase()];
if (help) console.log(`${pad} 💡 ${help}`);
});
node.children.forEach((child, i) => {
console.log(`${pad}Frame: ${child.src || child.url || child.name || child.id || `iframe ${i + 1}`}`);
printReasons(child, indent + 3);
});
};
const displayResults = () => {
const statusIcons = { 'no-blockers-detected': '🟢', 'likely-blocked': '🟠', blocked: '🔴' };
const statusColors = { 'no-blockers-detected': '#22c55e', 'likely-blocked': '#fb923c', blocked: '#ef4444' };
const statusText = {
'no-blockers-detected': 'No blockers detected',
'likely-blocked': 'Likely blocked',
blocked: 'Blocked (reported by the browser)',
};
console.group(
`%c${statusIcons[results.eligibility] || '⚪'} bfcache: ${statusText[results.eligibility] || 'Unknown'}`,
`color: ${statusColors[results.eligibility] || '#6b7280'}; font-weight: bold; font-size: 14px;`
);
const navEntry = performance.getEntriesByType('navigation')[0];
console.log('');
console.log('%c📊 Status:', 'font-weight: bold;');
if (results.wasRestored) {
console.log('%c ✅ This page was restored from bfcache', 'color: #22c55e;');
} else if (navEntry?.type === 'back_forward') {
console.log(' ℹ️ This page was loaded by a back/forward navigation, not restored from bfcache');
} else {
console.log(' ℹ️ This page was loaded normally. Restoration is reported by a pageshow event.');
}
if (navEntry) console.log(` Navigation type: ${navEntry.type}`);
if (results.blockingReasons.length > 0) {
console.log('');
console.log('%c🔍 Findings:', 'font-weight: bold;');
console.table(
results.blockingReasons.map((i) => ({
Severity: i.severity.toUpperCase(),
Finding: i.reason,
Detail: i.description,
}))
);
} else {
console.log('');
console.log('%c✅ No bfcache blockers detected', 'color: #22c55e; font-weight: bold;');
}
if (results.notRestoredReasons) {
console.log('');
console.group('%c🔬 NotRestoredReasons (reported by the browser)', 'font-weight: bold; color: #3b82f6;');
if (results.notRestoredReasons.reasons.length === 0 && results.notRestoredReasons.children.length === 0) {
console.log(' No reasons reported');
}
printReasons(results.notRestoredReasons, 3);
console.groupEnd();
}
console.log('');
console.log(
'%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.',
'color: #6b7280;'
);
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}`));
}
console.log('');
console.log('%c🧪 How to test:', 'font-weight: bold;');
console.log(' 1. Navigate to another page');
console.log(' 2. Click the browser Back button');
console.log(' 3. Run this snippet again to read NotRestoredReasons, or use DevTools → Application → Back/forward cache');
console.groupEnd();
};
const buildResult = () => ({
script: 'Back-Forward-Cache',
status: 'ok',
details: {
eligibility: results.eligibility,
wasRestored: results.wasRestored,
supported: results.supported,
navigationType: performance.getEntriesByType('navigation')[0]?.type ?? null,
notRestoredReasons: results.notRestoredReasons,
},
issues: results.blockingReasons.map((i) => ({
severity: i.severity === 'high' ? 'error' : i.severity === 'medium' ? 'warning' : 'info',
message: i.reason,
})),
});
// Expose function for manual check
window.checkBfcache = async () => {
await analyze();
displayResults();
return buildResult();
};
console.log('%c🚀 bfcache Analysis', 'font-weight: bold; font-size: 14px;');
console.log(
' Call %ccheckBfcache()%c anytime to re-run analysis.',
'font-family: monospace; background: #f3f4f6; padding: 2px 4px;',
''
);
await analyze();
displayResults();
return {
...buildResult(),
message: 'bfcache analysis complete. Call checkBfcache() to re-run analysis.',
};
})();
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 |
What the snippet detects
Several blockers are invisible to JavaScript, so the snippet separates what it can verify from what only the browser knows:
| Source | What it reports | Result |
|---|---|---|
NotRestoredReasons (after a back navigation) | Every reason the browser found, for the page and for each frame | blocked |
window.onunload set as a property | An unload handler | likely-blocked |
Cache-Control: no-store header | Read with a HEAD request to the current URL | likely-blocked |
| iframes, Service Worker | Context only, never a blocker by themselves | info |
When none of these apply, the result is no-blockers-detected. That is not a guarantee: unload listeners added with addEventListener, and open WebSocket, BroadcastChannel or IndexedDB connections, cannot be detected from JavaScript. They appear in NotRestoredReasons after a back navigation, or in DevTools → Application → Back/forward cache.
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
The header is part of the HTTP response of the page. A <meta http-equiv="Cache-Control"> tag is ignored by browsers, so it has no effect on bfcache either way.
Bad
Cache-Control: no-storeGood
Cache-Control: no-cacheKeep no-store only for responses that contain sensitive data, and use private or no-cache for the rest.
Issue 3: Open WebSocket
Bad
const ws = new WebSocket("wss://web.dev");
// Connection stays open, blocks bfcacheGood
const ws = new WebSocket("wss://web.dev");
window.addEventListener("pagehide", () => {
ws.close(); // Close on navigation
});
window.addEventListener("pageshow", (event) => {
if (event.persisted) {
// Reconnect after bfcache restore
ws = new WebSocket("wss://web.dev");
}
});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)
- Open your page and run the snippet. It reports the blockers it can detect and starts listening for the
pageshowevent. - Navigate to another page, for example by clicking a link.
- Press the browser Back button.
- Run the snippet again. The browser now reports
NotRestoredReasonsfor the page and its frames.
A pageshow event with persisted: true is the only reliable sign that a page was restored. The snippet logs it as soon as it happens, but only if it was running before the navigation.
Output when the page is restored
⚡ Page restored from bfcache!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
NotRestoredReasons API (Chrome 123+)
Chrome 123+ exposes notRestoredReasons on the navigation entry. After a back or forward navigation that was not served from bfcache, it lists why. This is the most accurate way to diagnose bfcache issues.
API structure
const navEntry = performance.getEntriesByType("navigation")[0];
const nrr = navEntry.notRestoredReasons;
if (nrr) {
console.log("URL:", nrr.url); // page URL
nrr.reasons.forEach((detail) => {
console.log("Reason:", detail.reason); // e.g. "unload-listener"
});
// Embedded frames, each with the same shape
nrr.children.forEach((child) => {
console.log("Frame:", child.src, child.reasons.map((d) => d.reason));
});
}Each entry of reasons is an object whose reason property holds the reason. The notRestoredReasons property is null when the page was restored, when the navigation was not a back or forward one, and in browsers without support.
The snippet walks the frame tree and returns it in details.notRestoredReasons, with one error issue for every reason and the frame it comes from.
Reason values
Common reason values
| Reason | What it means |
|---|---|
unload-listener | The page or a frame has an unload event listener |
response-cache-control-no-store | The page response has Cache-Control: no-store |
websocket | Open WebSocket connection |
broadcastchannel | Open BroadcastChannel |
indexeddb-connection | Open IndexedDB connection |
related-active-contents | A related window (popup, opener) blocks caching |
masked | The browser hides the exact reason, for example for a cross-origin frame |
Example with an embedded frame
{
"url": "https://example.com/",
"reasons": [{ "reason": "masked" }, { "reason": "unload-listener" }],
"children": [
{ "src": "/frame", "reasons": [{ "reason": "unload-listener" }], "children": [] }
]
}This tells us:
- The page was not restored from bfcache
- The page has an
unloadlistener - An embedded frame also has an
unloadlistener, and it blocks the whole page maskedmeans the browser does not disclose one of the reasons- Action: replace both
unloadlisteners withpagehide
Interpreting your results
| Result | Meaning | Next step |
|---|---|---|
blocked | The browser reported reasons after a back navigation | Fix each listed reason, in the page and in its frames |
likely-blocked | The snippet found an unload handler or Cache-Control: no-store | Remove it and test a back navigation to confirm |
no-blockers-detected | Nothing detectable was found | Test a back navigation; blockers that JavaScript cannot see only appear in NotRestoredReasons |
Testing workflow
notRestoredReasons describes the navigation that just happened, so a fix is only confirmed by a new cycle:
- Open the page and navigate away. The browser decides at this point whether the page goes into bfcache.
- Press Back and run the snippet. Read the reasons.
- Fix the blockers: remove
unloadlisteners, close connections onpagehide. - Repeat from step 1. A
pageshowevent withpersisted: trueconfirms the fix.
Chrome DevTools shortcut
- DevTools → Application → Back/forward cache
- Click "Test back/forward cache"
- The panel lists the blocking reasons immediately
Browser support
The snippet needs these features to run.
| Feature | Chrome | Edge | Firefox | Safari |
|---|---|---|---|---|
| PerformanceNavigationTiming (opens in a new tab) | 57 | 12 | 58 | 15 |
| PageTransitionEvent.persisted (opens in a new tab) | 4 | 12 | 11 | 5 |
| All of the above | 57 | 12 | 58 | 15 |
The snippet reports on these features. It runs without them, and its advice on each one applies where it is supported.
| Feature | Chrome | Edge | Firefox | Safari |
|---|---|---|---|---|
| PerformanceNavigationTiming.notRestoredReasons (opens in a new tab) | 125 | 125 |
Source: MDN browser compatibility data (opens in a new tab), version 8.1.4.
All modern browsers support bfcache, but diagnostics vary.
Example output
Page with an unload handler, run before navigating away
🚀 bfcache Analysis
🟠 bfcache: Likely blocked
📊 Status:
ℹ️ This page was loaded normally. Restoration is reported by a pageshow event.
Navigation type: navigate
🔍 Findings:
┌─────────┬──────────┬──────────────────────────────┬─────────────────────────────────────────────────────────────────┐
│ (index) │ Severity │ Finding │ Detail │
├─────────┼──────────┼──────────────────────────────┼─────────────────────────────────────────────────────────────────┤
│ 0 │ 'HIGH' │ 'window.onunload handler set'│ 'unload event listeners block bfcache. Use pagehide or ...' │
└─────────┴──────────┴──────────────────────────────┴─────────────────────────────────────────────────────────────────┘
💡 Recommendations:
1. unload event listeners block bfcache. Use pagehide or visibilitychange instead.Same page after navigating away and pressing back
The page has an unload listener and an iframe with its own unload listener.
🔴 bfcache: Blocked (reported by the browser)
📊 Status:
ℹ️ This page was loaded by a back/forward navigation, not restored from bfcache
Navigation type: back_forward
🔬 NotRestoredReasons (reported by the browser)
• masked
💡 The browser does not disclose the exact reason (for example, a cross-origin frame).
• unload-listener
💡 unload event listeners block bfcache. Use pagehide or visibilitychange instead.
Frame: /frame
• unload-listener
💡 unload event listeners block bfcache. Use pagehide or visibilitychange instead.The details.notRestoredReasons property of the returned object carries the same tree, and issues has one error per reason and frame.
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
Count restorations with the pageshow event: a restore fires it with persisted: true, while a regular back navigation loads the page again and reports notRestoredReasons.
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 why a back/forward navigation did not use bfcache
const navEntry = performance.getEntriesByType("navigation")[0];
if (navEntry && navEntry.notRestoredReasons) {
const reasons = navEntry.notRestoredReasons.reasons || [];
gtag("event", "bfcache_not_restored", {
reasons: reasons.map((detail) => detail.reason).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