Fix back/forward cache issues
bfcache makes back button navigation instant. Fix it first. If your page prevents bfcache, users wait for a full reload instead.
What's happening
The back/forward cache (bfcache) stores page snapshots in memory. When users hit back or forward, the browser restores the snapshot instantly. No network requests, no JavaScript re-execution, no layout recalculation. It's the fastest navigation possible. Use it.
Unload handlers, WebSockets, and certain HTTP headers block bfcache. When blocked, the back button triggers a full page reload and adds seconds to the navigation.
Lighthouse tests bfcache by simulating a back/forward navigation. It reports specific blocking reasons if restoration fails.
Diagnose
Chrome DevTools Application tab
- Open DevTools and go to Application tab.
- Find "Back/forward cache" in the sidebar.
- Click "Test back/forward cache."
- Review the blocking reasons if the test fails.
The panel categorizes blockers as:
- Actionable: Issues in your code you must fix.
- Pending browser support: Browser limitations that future updates may resolve.
- Not actionable: Inherent limitations.
Lighthouse audit
The "Page prevented back/forward cache restoration" audit shows:
- Each blocking reason with a description.
- The frame URL where Lighthouse detected the blocker.
- Whether the blocker is actionable.
Fix
1. Remove unload event handlers
The unload event is the most common bfcache blocker. It's unreliable. Mobile browsers often don't fire it.
// Bad: blocks bfcache
window.addEventListener('unload', () => {
sendAnalytics()
})
// Good: use pagehide with persisted check
window.addEventListener('pagehide', (event) => {
if (!event.persisted) {
// Page is being destroyed, not cached
sendAnalytics()
}
})
// Better: use visibilitychange for analytics
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
navigator.sendBeacon('/analytics', data)
}
})
Also check for beforeunload handlers that aren't conditionally added:
// Bad: always blocks bfcache
window.addEventListener('beforeunload', handleBeforeUnload)
// Good: only add when user has unsaved changes
function enableUnsavedWarning() {
window.addEventListener('beforeunload', handleBeforeUnload)
}
function disableUnsavedWarning() {
window.removeEventListener('beforeunload', handleBeforeUnload)
}
2. Close open connections
Persistent connections block bfcache.
WebSockets:
// Close WebSocket when page is hidden
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
socket.close()
}
})
// Reconnect when page becomes visible
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible' && !socket.connected) {
socket.connect()
}
})
IndexedDB transactions:
// Check that transactions complete before page hide
window.addEventListener('pagehide', () => {
// Let any pending transactions commit
db.close()
})
3. Avoid blocking HTTP headers
These response headers prevent bfcache:
Cache-Control: no-store
Cache-Control: no-cache, no-store, must-revalidate
Cache-Control: private, max-age=0, must-revalidate
The no-store directive tells browsers never to cache the response. This extends to bfcache. Use private instead to prevent CDN caching while allowing bfcache.
4. Handle pageshow for restored pages
Refresh stale state when a page is restored from bfcache:
window.addEventListener('pageshow', (event) => {
if (event.persisted) {
// Page was restored from bfcache
// Refresh stale data
refreshUserSession()
updateTimestamps()
reconnectWebSocket()
}
})
5. Address third-party blockers
Third-party scripts add unload handlers. Check for:
- Analytics scripts (older versions of Google Analytics)
- Chat widgets
- Ad scripts
- Social media embeds
// Audit what's listening to unload
getEventListeners(window).unload
// Returns array of unload handlers with source info
Update third-party scripts or defer their loading until interaction:
<!-- Defer chat widget until user interacts -->
<script>
document.addEventListener('click', () => {
loadChatWidget()
}, { once: true })
</script>
Verify the fix
- Open DevTools Application tab.
- Navigate to "Back/forward cache."
- Click "Test back/forward cache."
- Resolve all actionable blockers.
- Run Lighthouse to verify the audit passes.
- Manually test: navigate away, then hit back. The page must restore instantly without network activity.
Common mistakes
- Third-party analytics: Older analytics libraries add unload handlers. Update them immediately.
- Browser extensions: Extensions inject blocking code. Test in incognito mode with extensions disabled.
- Service worker fetch handlers: Complex fetch handlers block bfcache. Keep service worker logic simple.
- Cross-origin iframes: A blocked iframe blocks the whole page. Check all third-party embeds.
- Testing in DevTools with throttling: Network throttling interferes with bfcache testing. Test without throttling.
Pending browser support issues
Some bfcache blockers aren't actionable yet:
- BroadcastChannel: Open BroadcastChannels block bfcache in some browsers.
- SharedWorker: SharedWorker prevents bfcache.
- Keepalive fetch: Pending fetch requests with keepalive block restoration.
Monitor the Chrome status page for changes.
Test your entire site
Different pages have different third-party scripts and connection patterns. Test bfcache across your entire site to catch all blockers.