When users hit the back button, bfcache makes it instant by restoring the page from memory. If your page prevents bfcache, users wait for a full reload instead.
The back/forward cache (bfcache) stores complete page snapshots in memory when users navigate away. When they hit back or forward, the browser instantly restores the snapshot—no network requests, no JavaScript re-execution, no layout recalculation. It's the fastest navigation possible.
But many things can block bfcache: unload event handlers, open WebSocket connections, certain HTTP headers, and more. When blocked, the back button triggers a full page reload, adding seconds to what should be instant.
Lighthouse tests bfcache by simulating a back/forward navigation and checking if the page restored from cache. If restoration fails, it reports the specific blocking reasons.
The panel categorizes blockers as:
The "Page prevented back/forward cache restoration" audit shows:
unload Event HandlersThe unload event is the most common bfcache blocker. It's unreliable anyway—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)
}
Persistent connections block bfcache because they can't be frozen and restored.
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:
// Ensure transactions complete before page hide
window.addEventListener('pagehide', () => {
// Let any pending transactions commit
db.close()
})
Certain 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 to never cache the response, which extends to bfcache. Use private instead if you want to prevent CDN caching but allow bfcache.
pageshow for Restored PagesWhen a page is restored from bfcache, some state may be stale:
window.addEventListener('pageshow', (event) => {
if (event.persisted) {
// Page was restored from bfcache
// Refresh stale data
refreshUserSession()
updateTimestamps()
reconnectWebSocket()
}
})
Third-party scripts often add unload handlers. Check for:
// 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>
visibilitychange and sendBeacon.Some bfcache blockers aren't actionable yet:
These may be addressed in future browser updates. Monitor the Chrome status page for changes.
Different pages have different third-party scripts and connection patterns. Test bfcache across your entire site to catch all blockers.
Scan Your Site with Unlighthouse