---
title: "Fix back/forward cache issues"
description: "Make back button navigation instant with bfcache. Learn common blockers and how to fix them."
canonical_url: "https://unlighthouse.dev/learn-lighthouse/best-practices/bf-cache"
last_updated: "2025-01-18"
---

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.

```js
// 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:

```js
// 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**:

```js
// 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**:

```js
// 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:

```http
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:

```js
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

```js
// 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:

```html
<!-- 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.
