Back/Forward Cache (bfcache): The Complete Guide to Instant Back Navigation

Learn how to optimize your site for the browser's back/forward cache (bfcache) to achieve instant back navigation, improve Core Web Vitals, and fix common blockers like unload events and Cache-Control headers.

What Is the Back/Forward Cache (bfcache)?

The back/forward cache — commonly called bfcache — is a browser optimization that stores a complete snapshot of a web page in memory when the user navigates away. But unlike the HTTP cache, which stores individual resources like images and scripts, bfcache preserves the entire page state: the DOM tree, JavaScript heap, scroll position, form input values, even in-flight timers. When someone hits the back or forward button, the browser restores this snapshot instantly instead of fetching and rendering everything from scratch.

The result? Navigation that feels literally instant — zero network requests, zero parsing, zero layout, zero paint. The page appears exactly as the user left it, typically in under 10 milliseconds.

Every major browser now supports bfcache — Chrome, Firefox, Safari, and Edge. And as of early 2025, Chrome expanded bfcache eligibility dramatically by allowing it even on pages served with Cache-Control: no-store. That single change removed the largest blocker that had previously prevented about 17% of mobile history navigations from being cached.

Why bfcache Matters for Web Performance

If you think bfcache is some niche optimization that doesn't apply to your site, the numbers tell a different story:

  • 10% of desktop navigations and 20% of mobile navigations are back or forward actions (per Chrome usage data)
  • bfcache-restored pages have near-zero LCP, minimal CLS, and immediate interactivity — essentially perfect Core Web Vitals scores for those page loads
  • Sites optimized for bfcache report measurably lower bounce rates and higher engagement, especially on e-commerce product pages where users frequently compare items by navigating back and forth
  • Unlike most performance work that requires weeks of engineering effort, fixing bfcache blockers is often a matter of removing a single event listener or changing one HTTP header

Honestly, bfcache is one of those rare cases where a small code change can produce a massive, measurable performance improvement for a significant chunk of your traffic. I've seen sites go from "decent" to "wow" Core Web Vitals just by cleaning up their unload handlers.

How bfcache Works Under the Hood

Understanding the bfcache lifecycle helps you debug problems and write compatible code. Here's what happens step by step:

  1. User navigates away: Instead of destroying the page, the browser fires pagehide and visibilitychange events, then freezes JavaScript execution and stores the entire page state in memory
  2. Page sits in bfcache: All pending timers, promises, and task queue items are paused. The page consumes memory but zero CPU
  3. User navigates back: The browser restores the page from memory, resumes JavaScript execution, fires pageshow with event.persisted = true, and the page appears immediately
  4. Eviction: If the browser needs memory, or the page has been cached too long (typically 10 minutes, or 3 minutes for no-store pages in Chrome), the snapshot gets discarded and the next back navigation triggers a full reload

Here's the crucial part: the unload event is never fired for pages entering bfcache. This is the root cause of most bfcache incompatibility — code that relies on unload forces the browser to skip bfcache entirely.

Chrome's 2025 Game-Changer: bfcache for no-store Pages

One of the most significant bfcache developments happened in early 2025, when Chrome completed its rollout of bfcache support for pages served with Cache-Control: no-store. This deserves special attention because no-store was previously the single largest bfcache blocker.

What changed

Chrome now allows bfcache for no-store pages, but with two safety mechanisms:

  • Cookie-change eviction: If any cookies change after the user navigates away (say, during logout), the cached page is automatically evicted from bfcache
  • Shortened timeout: Pages with no-store are kept in bfcache for a maximum of 3 minutes instead of the standard 10

What this means for you

If your site uses Cache-Control: no-store broadly (and many sites do, even for non-sensitive pages), you may already be benefiting from bfcache without any code changes. That's the good news.

The caveat: you should verify that your logout flows properly modify cookies so Chrome correctly evicts authenticated pages when a user signs out. For pages that genuinely contain sensitive information and should never be restored from bfcache, just make sure your logout flow updates or deletes session cookies. Chrome handles the rest.

How to Test Your Pages for bfcache Eligibility

Before fixing anything, you need to know where you stand. There are three solid ways to test bfcache compatibility.

Method 1: Chrome DevTools (quickest)

  1. Open DevTools on your page
  2. Navigate to Application → Background services → Back/forward cache
  3. Click "Test back/forward cache"
  4. Chrome navigates to chrome://terms/ and back. If the test passes, you'll see "Restored from back-forward cache." If not, you get a list of specific failure reasons

Failure reasons are categorized as:

  • Actionable: Issues you can fix (e.g., unload event listeners)
  • Pending Support: Browser limitations that'll be lifted in future versions
  • Not Actionable: Issues outside your control (e.g., browser extensions)

Quick tip: Test in an Incognito window or Guest profile to eliminate interference from browser extensions. I can't tell you how many times I've seen a "failure" that turned out to be a password manager extension.

Method 2: Lighthouse audit

Lighthouse includes a dedicated bfcache audit. Run a Lighthouse performance report and look for the "Page prevented back/forward cache restoration" diagnostic. It reports the same failure reasons as DevTools but is easier to automate in CI/CD pipelines.

Method 3: The notRestoredReasons API (field data)

For production monitoring, the notRestoredReasons API (available since Chrome 123) lets you collect bfcache failure data from real users:

// Monitor bfcache failures in the field
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.type === 'back_forward' && entry.notRestoredReasons) {
      const reasons = entry.notRestoredReasons;
      // Send to your analytics endpoint
      navigator.sendBeacon('/analytics/bfcache', JSON.stringify({
        url: entry.name,
        reasons: reasons.reasons,
        children: reasons.children // iframe-level reasons
      }));
    }
  }
});
observer.observe({ type: 'navigation', buffered: true });

This gives you real-world data on exactly which pages are being blocked and why — far more actionable than lab tests alone.

Common bfcache Blockers and How to Fix Them

So, let's get into the fixes. Here are the most frequent reasons pages fail bfcache, ordered by how often they show up in the wild, with exact code fixes for each.

1. The unload event listener

This is the number one bfcache killer. Any unload listener — whether in your own code or buried in a third-party script — makes the page ineligible for bfcache in Chrome and Firefox on desktop.

Before (broken):

// DON'T: This prevents bfcache
window.addEventListener('unload', () => {
  sendAnalyticsBeacon();
  cleanupResources();
});

After (fixed):

// DO: Use pagehide instead — fires in all cases unload does,
// PLUS fires when the page enters bfcache
window.addEventListener('pagehide', (event) => {
  sendAnalyticsBeacon();

  if (!event.persisted) {
    // Page is truly being destroyed, not entering bfcache
    cleanupResources();
  }
});

The pagehide event fires in every case where unload fires, plus it fires when the page enters bfcache. Check event.persisted to tell the two scenarios apart.

2. Open WebSocket, WebTransport, or WebRTC connections

Pages with active real-time connections can't be frozen in bfcache because those connections have server-side state.

Fix: Close connections in pagehide and reconnect in pageshow:

let socket = null;

function connectWebSocket() {
  socket = new WebSocket('wss://api.example.com/ws');
  socket.addEventListener('message', handleMessage);
}

window.addEventListener('pagehide', () => {
  if (socket) {
    socket.close();
    socket = null;
  }
});

window.addEventListener('pageshow', (event) => {
  if (event.persisted) {
    // Reconnect after bfcache restore
    connectWebSocket();
  }
});

// Initial connection
connectWebSocket();

3. Cache-Control: no-store (legacy concern)

As of Chrome's 2025 changes, no-store alone no longer blocks bfcache in Chrome. However, other browsers (Firefox, Safari) may still treat it as a blocker. If you're using no-store on non-sensitive pages, consider switching to no-cache or a short max-age for broader compatibility:

# Instead of this (blocks bfcache in some browsers):
Cache-Control: no-store

# Use this for non-sensitive dynamic pages:
Cache-Control: no-cache, max-age=0, must-revalidate

# Or this for pages that can be cached briefly:
Cache-Control: private, max-age=60

4. Active IndexedDB transactions

Pages with open IndexedDB transactions get blocked from bfcache because uncommitted transactions could leave the database in an inconsistent state. Makes sense when you think about it.

Fix: Make sure all IndexedDB transactions complete before navigation. If you're using IndexedDB for caching, close the database connection in pagehide:

let db = null;

async function openDatabase() {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open('myApp', 1);
    request.onsuccess = () => {
      db = request.result;
      resolve(db);
    };
    request.onerror = () => reject(request.error);
  });
}

window.addEventListener('pagehide', () => {
  if (db) {
    db.close();
    db = null;
  }
});

window.addEventListener('pageshow', async (event) => {
  if (event.persisted && !db) {
    await openDatabase();
  }
});

5. The beforeunload listener (partial blocker)

In modern browsers, beforeunload no longer fully blocks bfcache, but it can still cause issues. The fix is straightforward: only add it when genuinely needed (like unsaved form changes) and remove it immediately after:

const form = document.querySelector('#editor-form');
let hasUnsavedChanges = false;

function onBeforeUnload(event) {
  event.preventDefault();
  // Modern browsers ignore custom messages
  return '';
}

form.addEventListener('input', () => {
  if (!hasUnsavedChanges) {
    hasUnsavedChanges = true;
    window.addEventListener('beforeunload', onBeforeUnload);
  }
});

form.addEventListener('submit', () => {
  hasUnsavedChanges = false;
  window.removeEventListener('beforeunload', onBeforeUnload);
});

6. Third-party scripts using unload

Even if your own code is squeaky clean, third-party scripts are a common source of unload listeners. Known offenders include older versions of analytics libraries, hCaptcha iframes, marketing pixels, and some live chat widgets.

How to identify them:

// Run this in the console to find all unload listeners
getEventListeners(window).unload?.forEach(listener => {
  console.log('unload listener:', listener.listener.toString().substring(0, 200));
});

Fix options:

  • Update the library to a version that uses pagehide instead
  • Lazy-load the script so it only runs on pages where it's actually needed
  • Load the script in a sandboxed iframe with rel="noopener" so its unload listener doesn't block the parent page
  • Use a Partytown-style approach to run the script off the main thread

Framework-Specific Fixes

Next.js

Next.js has two common bfcache issues worth calling out:

Issue 1: no-store header on all HTML responses. By default, Next.js sends Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate for HTML pages. While Chrome now handles no-store pages, you can improve cross-browser compatibility with middleware:

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const response = NextResponse.next();

  // Only override HTML page responses, not API routes
  if (!request.nextUrl.pathname.startsWith('/api')) {
    response.headers.set(
      'Cache-Control',
      'public, no-cache, max-age=0, must-revalidate'
    );
  }

  return response;
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};

Issue 2: Router state not updating on bfcache restore. When a user navigates back from an external site, the Next.js router may hold stale state. You can handle this with a pageshow listener:

// app/layout.tsx (App Router) or pages/_app.tsx (Pages Router)
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';

export default function RootLayout({ children }) {
  const router = useRouter();

  useEffect(() => {
    const handlePageShow = (event: PageTransitionEvent) => {
      if (event.persisted) {
        // Force router to re-sync after bfcache restore
        router.refresh();
      }
    };

    window.addEventListener('pageshow', handlePageShow);
    return () => window.removeEventListener('pageshow', handlePageShow);
  }, [router]);

  return <html><body>{children}</body></html>;
}

React SPAs with client-side routing

Single-page apps using React Router or similar libraries typically work fine with bfcache since the browser caches the shell page. Still, you should refresh stale data on restore:

import { useEffect } from 'react';

function useBfcacheRestore(onRestore: () => void) {
  useEffect(() => {
    const handler = (event: PageTransitionEvent) => {
      if (event.persisted) {
        onRestore();
      }
    };
    window.addEventListener('pageshow', handler);
    return () => window.removeEventListener('pageshow', handler);
  }, [onRestore]);
}

// Usage in a component:
function ProductPage({ productId }) {
  const [product, setProduct] = useState(null);

  const fetchProduct = useCallback(async () => {
    const data = await fetch(`/api/products/${productId}`).then(r => r.json());
    setProduct(data);
  }, [productId]);

  useEffect(() => { fetchProduct(); }, [fetchProduct]);

  // Re-fetch when restored from bfcache
  useBfcacheRestore(fetchProduct);

  return product ? <ProductDetails product={product} /> : <Loading />;
}

Fix Your Analytics for bfcache

One of the most common (and most overlooked) side effects of bfcache is underreported page views. When a user navigates back and the page restores from bfcache, the standard load event doesn't fire. If your analytics only track page views on load, you're missing 10-20% of actual page views. That's a big blind spot.

Google Analytics (gtag.js)

// Standard page view on initial load
gtag('event', 'page_view');

// Track bfcache-restored page views
window.addEventListener('pageshow', (event) => {
  if (event.persisted) {
    gtag('event', 'page_view', {
      page_title: document.title,
      page_location: window.location.href,
      send_to: 'G-XXXXXXXXXX'
    });
  }
});

Google Tag Manager (dataLayer)

window.addEventListener('pageshow', (event) => {
  if (event.persisted) {
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push({
      event: 'page_view',
      pv_origin: 'bfcache_restore'
    });
  }
});

Custom analytics or RUM tools

window.addEventListener('pageshow', (event) => {
  if (event.persisted) {
    // Use sendBeacon for reliable delivery during navigation
    navigator.sendBeacon('/analytics/pageview', JSON.stringify({
      url: window.location.href,
      referrer: document.referrer,
      type: 'bfcache_restore',
      timestamp: Date.now()
    }));
  }
});

bfcache and Core Web Vitals: What Gets Reported

Understanding how bfcache affects Core Web Vitals measurement is essential for accurate performance monitoring. Here's the breakdown:

  • LCP: For bfcache-restored pages, the page appears instantly. Chrome reports a near-zero LCP for these navigations. The web-vitals JavaScript library creates new metric instances for bfcache restores and labels the navigation type as 'back-forward-cache'
  • CLS: Since the page is restored in its final state with no new layout, there's typically zero layout shift on restore. Any CLS that accumulates after restore (e.g., from JavaScript that updates the DOM in a pageshow handler) is counted separately
  • INP: The page is immediately interactive after bfcache restore, so the initial interaction response time is minimal. INP tracks all interactions during the restored session normally

The key takeaway: CrUX includes bfcache navigations in its data, which means improving bfcache eligibility directly improves your site-level Core Web Vitals as reported in Google Search Console and PageSpeed Insights. It's basically free performance points.

A Complete bfcache Optimization Checklist

Use this checklist to systematically audit and fix your site for bfcache eligibility:

  1. Audit unload listeners: Search your codebase and third-party scripts for addEventListener('unload' and replace every instance with pagehide
  2. Review Cache-Control headers: Switch non-sensitive pages from no-store to no-cache for cross-browser bfcache compatibility
  3. Close open connections: Tear down WebSockets, WebTransport, and WebRTC connections in pagehide and reconnect in pageshow
  4. Complete IndexedDB transactions: Close database connections in pagehide to prevent transaction-related blocks
  5. Audit iframes: Third-party iframes with unload listeners (hCaptcha, older YouTube embeds) block the parent page — lazy-load or conditionally render them
  6. Add analytics tracking: Listen for pageshow with event.persisted to track bfcache-restored page views
  7. Handle stale data: Refresh time-sensitive content (prices, stock levels, auth status) in your pageshow handler
  8. Test in DevTools: Use the Application → Back/forward cache panel in Chrome DevTools to verify each page passes
  9. Monitor in production: Deploy the notRestoredReasons API to collect field data on bfcache failures
  10. Verify logout flows: Make sure signing out modifies cookies so Chrome evicts authenticated pages from bfcache

Frequently Asked Questions

Does bfcache work on all browsers?

Yes — all major browsers support it, including Chrome (since v96), Firefox, Safari, and Edge. As of 2025, roughly 98% of browsers in use support bfcache. That said, eligibility varies: a page might be bfcache-eligible in one browser but blocked in another depending on the specific features and APIs it uses.

How is bfcache different from the HTTP cache?

The HTTP cache stores individual resources (HTML files, images, JavaScript bundles) and serves them from disk or memory on subsequent requests. bfcache is a whole different beast — it stores a complete in-memory snapshot of the entire page, including the JavaScript heap and DOM state. A bfcache restore is always faster than even a fully HTTP-cached page load because it skips parsing, compilation, layout, and paint entirely.

Does bfcache affect my SEO or Core Web Vitals scores?

Yes — and in a good way. CrUX (the data source for Core Web Vitals in Google Search Console) includes bfcache navigations. Since bfcache restorations have near-zero LCP, minimal CLS, and excellent INP, every bfcache-eligible navigation improves your aggregate Core Web Vitals scores. That can give you a real competitive edge in search rankings.

Will bfcache show outdated content to users?

bfcache restores the page exactly as the user left it. For static or semi-static content, that's perfect. For dynamic content (live prices, stock status, chat messages), listen for the pageshow event with event.persisted === true and refresh only the data that may have changed. You get to keep the instant navigation while ensuring content accuracy.

Can I opt out of bfcache for specific pages?

There's no official HTTP header to opt out of bfcache. However, if a page uses features that block bfcache (like unload listeners or active WebSocket connections), it'll be excluded automatically. For sensitive pages, the most reliable approach is to ensure your logout flow modifies cookies — Chrome will then evict the page from bfcache on its own.

About the Author Editorial Team

Our team of expert writers and editors.