Fix back/forward cache issues

Make back button navigation instant with bfcache. Learn common blockers and how to fix them.
Harlan WiltonHarlan Wilton3 min read Published

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

  1. Open DevTools and go to Application tab.
  2. Find "Back/forward cache" in the sidebar.
  3. Click "Test back/forward cache."
  4. 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

  1. Open DevTools Application tab.
  2. Navigate to "Back/forward cache."
  3. Click "Test back/forward cache."
  4. Resolve all actionable blockers.
  5. Run Lighthouse to verify the audit passes.
  6. 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.

\n","html",[4376,5246,5247,5252,5264,5290,5297,5319],{"__ignoreMap":1843},[4395,5248,5249],{"class":4397,"line":1766},[4395,5250,5251],{"class":4400},"\n",[4395,5253,5254,5257,5261],{"class":4397,"line":1756},[4395,5255,5256],{"class":4410},"<",[4395,5258,5260],{"class":5259},"sV-QU","script",[4395,5262,5263],{"class":4410},">\n",[4395,5265,5266,5269,5271,5273,5275,5277,5280,5282,5284,5286,5288],{"class":4397,"line":1828},[4395,5267,5268],{"class":4406}," document",[4395,5270,4411],{"class":4410},[4395,5272,4415],{"class":4414},[4395,5274,4418],{"class":4406},[4395,5276,4422],{"class":4421},[4395,5278,5279],{"class":4425},"click",[4395,5281,4422],{"class":4421},[4395,5283,4430],{"class":4410},[4395,5285,4433],{"class":4410},[4395,5287,4437],{"class":4436},[4395,5289,4440],{"class":4410},[4395,5291,5292,5295],{"class":4397,"line":1749},[4395,5293,5294],{"class":4414}," loadChatWidget",[4395,5296,4449],{"class":4448},[4395,5298,5299,5302,5305,5308,5310,5314,5317],{"class":4397,"line":1984},[4395,5300,5301],{"class":4410}," },",[4395,5303,5304],{"class":4410}," {",[4395,5306,5307],{"class":4448}," once",[4395,5309,4806],{"class":4410},[4395,5311,5313],{"class":5312},"sGFTI"," true",[4395,5315,5316],{"class":4410}," }",[4395,5318,4457],{"class":4406},[4395,5320,5321,5324,5326],{"class":4397,"line":4466},[4395,5322,5323],{"class":4410},"