Fix Back/Forward Cache Issues for Better Best Practices Score
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.
What's Happening
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.
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 can fix
- Pending browser support: Browser limitations that may be resolved in future updates
- Not actionable: Inherent limitations (e.g., page is currently open in another tab)
Lighthouse Audit
The "Page prevented back/forward cache restoration" audit shows:
- Each blocking reason with a description
- The frame URL where the blocker was detected
- Whether the blocker is actionable
Fix
1. Remove unload Event Handlers
The 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)
}
2. Close Open Connections
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()
})
3. Avoid Blocking HTTP Headers
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.
4. Handle pageshow for Restored Pages
When 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()
}
})
5. Address Third-Party Blockers
Third-party scripts often 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"
- Confirm all actionable blockers are resolved
- Run Lighthouse to verify the audit passes
- Manually test: navigate away, then hit back—page should restore instantly without network activity
Common Mistakes
- Third-party analytics — Older analytics libraries add unload handlers. Update to latest versions that use
visibilitychangeandsendBeacon. - Browser extensions — Extensions can inject blocking code. Test in incognito mode with extensions disabled.
- Service worker fetch handlers — Complex fetch handlers can block bfcache. Keep service worker logic simple.
- Cross-origin iframes — Iframes from other origins have their own bfcache eligibility. A blocked iframe blocks the whole page.
- Testing in DevTools with throttling — Network throttling can interfere with bfcache testing. Test without throttling.
Pending Browser Support Issues
Some bfcache blockers aren't actionable yet:
- BroadcastChannel: Having an open BroadcastChannel blocks bfcache in some browsers
- SharedWorker: Using SharedWorker currently prevents bfcache
- Keepalive fetch: Pending fetch requests with keepalive block restoration
These may be addressed in future browser updates. 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.