Loading
Back/Forward Cache (bfcache)

Back/forward cache (bfcache)

Chrome 57+ Edge 12+ Firefox 58+ Safari 15+ Details

Analyzes Back/Forward Cache (bfcache) (opens in a new tab) to determine if your page is eligible for instant back/forward navigations. When bfcache works, users get instant (0ms) page loads when using browser back/forward buttons, dramatically improving perceived performance.

Why this matters

bfcache is one of the most impactful performance optimizations available - it can make navigation instantaneous. However, many pages are unknowingly ineligible due to common patterns like unload handlers, Cache-Control: no-store, or open connections. This snippet reports the blockers it can detect and reads the NotRestoredReasons the browser reports after a back navigation, including those of embedded frames.

bfcache impact

Navigation TypeLoad TimeUser Experience
Normal navigation1-3sWait, spinner
bfcache restoration0msInstant

How bfcache works

Sequence of a navigation from Page A to Page B and back: the browser stores Page A in the bfcache when it is eligible, then restores it instantly on back navigation, or discards it and reloads it in fullSteps: User to Page A: Visit Page A; Note: Page loads normally; User to Browser: Navigate to Page B; Browser to Browser: Check bfcache eligibility; alt Page is eligible; Browser to Browser: Store Page A in bfcache; Note: Full page state preserved (DOM, JS state, scroll); else Page is ineligible; Browser to Browser: Discard Page A; Note: Reasons logged; Browser to Page B: Load Page B; Note: User on Page B; User to Browser: Click Back button; alt Page in bfcache; Browser to Page A: Restore from bfcache; Note: Instant restoration (0ms); Browser to Page A: Fire pageshow event with persisted=true; else Page not cached; Browser to Page A: Full page reload; Note: Normal load (1-3s).Visit Page APage loads normallyNavigate to Page BCheck bfcache eligibilityalt[Page is eligible][Page is ineligible]Store Page A in bfcacheFull page state preserved(DOM, JS state, scroll)Discard Page AReasons loggedLoad Page BUser on Page BClick Back buttonalt[Page in bfcache][Page not cached]Restore from bfcacheInstant restoration (0ms)Fire pageshow eventwith persisted=trueFull page reloadNormal load (1-3s)UserBrowserPage APage B

Common blocking reasons

Flow from leaving a page to the bfcache check: an eligible page is stored and restored instantly on back navigation, while a blocked page is discarded for one of six common reasons (unload handlers, no-store, open connections, service worker fetch handler, unfinished requests, embedded pages) and fully reloadedConnections: Navigate away from page to bfcache Eligibility Check; bfcache Eligibility Check to Store in bfcache (Eligible); bfcache Eligibility Check to Why blocked? (Blocked); Why blocked? to Common reasons; Common reasons to Page discarded; Store in bfcache to User navigates back; User navigates back to Instant restore; Page discarded to User navigates back; User navigates back to Full reload.Navigate away from pagebfcache Eligibility CheckStore in bfcacheWhy blocked?Common reasonsunload/beforeunload handlersCache-Control: no-storeOpen connections (WebSocket,WebRTC)Service Worker with fetch handlerUnfinished network requestsEmbedded pages with issuesUser navigates backInstant restorePage discardedUser navigates backFull reloadEligibleBlocked
Flow from leaving a page to the bfcache check: an eligible page is stored and restored instantly on back navigation, while a blocked page is discarded for one of six common reasons (unload handlers, no-store, open connections, service worker fetch handler, unfinished requests, embedded pages) and fully reloadedConnections: Navigate away from page to bfcache Eligibility Check; bfcache Eligibility Check to Store in bfcache (Eligible); bfcache Eligibility Check to Why blocked? (Blocked); Why blocked? to Common reasons; Common reasons to Page discarded; Store in bfcache to User navigates back; User navigates back to Instant restore; Page discarded to User navigates back; User navigates back to Full reload.Navigate awayfrom pagebfcacheEligibilityCheckStore in bfcacheWhy blocked?Common reasonsunload/beforeunloadhandlersCache-Control:no-storeOpen connections(WebSocket,WebRTC)Service Workerwith fetch handlerUnfinished networkrequestsEmbedded pageswith issuesUser navigatesbackInstant restorePage discardedUser navigatesbackFull reloadEligibleBlocked

Snippet

// Back/Forward Cache (bfcache) Analysis
// https://webperf-snippets.nucliweb.net

(async () => {
  // Only what the browser can report is analysed. Several blockers cannot be detected from
  // JavaScript (unload listeners added with addEventListener, open WebSocket, BroadcastChannel
  // or IndexedDB connections). After a back navigation, the NotRestoredReasons API reports
  // the blockers the browser found, including those of embedded frames.
  const results = {
    supported: 'PerformanceNavigationTiming' in window,
    wasRestored: false,
    eligibility: null,
    blockingReasons: [],
    notRestoredReasons: null,
    recommendations: [],
  };

  // A pageshow event with persisted = true is the only reliable sign of a bfcache restore
  window.addEventListener('pageshow', (event) => {
    if (event.persisted) {
      results.wasRestored = true;
      console.log(
        '%c⚡ Page restored from bfcache!',
        'color: #22c55e; font-weight: bold; font-size: 14px;'
      );
    }
  });

  const REASON_HELP = {
    'unload-listener': 'unload event listeners block bfcache. Use pagehide or visibilitychange instead.',
    'response-cache-control-no-store': 'Cache-Control: no-store on the page response prevents caching. Use no-cache instead.',
    'websocket': 'Open WebSocket connections prevent bfcache. Close them on pagehide.',
    'broadcastchannel': 'Open BroadcastChannel instances prevent bfcache. Close them on pagehide.',
    'indexeddb-connection': 'Open IndexedDB connections prevent bfcache. Close them on pagehide.',
    'masked': 'The browser does not disclose the exact reason (for example, a cross-origin frame).',
  };

  const serializeReasons = (node) =>
    node && {
      url: node.url ?? null,
      src: node.src ?? null,
      id: node.id ?? null,
      name: node.name ?? null,
      // Each entry is a NotRestoredReasonDetails object with a reason property
      // (plain strings are accepted as well)
      reasons: (node.reasons || []).map((r) => (typeof r === 'string' ? r : r?.reason)).filter(Boolean),
      children: (node.children || []).map(serializeReasons),
    };

  const flattenReasons = (node, frame) => [
    ...node.reasons.map((reason) => ({ reason, frame })),
    ...node.children.flatMap((child, i) =>
      flattenReasons(child, child.src || child.url || child.name || child.id || `iframe ${i + 1}`)
    ),
  ];

  const analyze = async () => {
    const issues = [];
    const navEntry = performance.getEntriesByType('navigation')[0];

    // 1. Blockers reported by the browser (after a back/forward navigation)
    results.notRestoredReasons = serializeReasons(navEntry?.notRestoredReasons);
    const confirmed = results.notRestoredReasons
      ? flattenReasons(results.notRestoredReasons, results.notRestoredReasons.url || 'main frame')
      : [];
    confirmed.forEach(({ reason, frame }) => {
      issues.push({
        reason: `${reason} (${frame})`,
        severity: 'high',
        source: 'browser',
        description: REASON_HELP[reason.toLowerCase()] || 'Reported by the browser as a bfcache blocker',
      });
    });

    // 2. unload handler assigned as a property. Listeners added with addEventListener
    // cannot be detected from JavaScript.
    if (window.onunload !== null && !confirmed.some(({ reason }) => reason === 'unload-listener')) {
      issues.push({
        reason: 'window.onunload handler set',
        severity: 'high',
        source: 'detected',
        description: REASON_HELP['unload-listener'],
      });
    }

    // 3. Cache-Control: no-store on the page response. A HEAD request to the current URL
    // exposes the header; the value can differ from the original navigation.
    if (/^https?:$/.test(location.protocol)) {
      try {
        const res = await fetch(location.href, { method: 'HEAD', credentials: 'same-origin' });
        const cacheControl = res.headers.get('cache-control') || '';
        if (/no-store/i.test(cacheControl) && !confirmed.some(({ reason }) => reason === 'response-cache-control-no-store')) {
          issues.push({
            reason: 'Cache-Control: no-store on the page response',
            severity: 'medium',
            source: 'detected',
            description: REASON_HELP['response-cache-control-no-store'],
          });
        }
      } catch {
        // The header could not be read; nothing is reported
      }
    }

    // 4. Context that is worth knowing but is not a blocker by itself
    const iframeCount = document.querySelectorAll('iframe').length;
    if (iframeCount > 0) {
      issues.push({
        reason: `${iframeCount} iframe(s) on the page`,
        severity: 'info',
        source: 'note',
        description: 'A bfcache blocker inside an iframe blocks the whole page. See notRestoredReasons after a back navigation.',
      });
    }

    if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
      issues.push({
        reason: 'Service Worker controls the page',
        severity: 'info',
        source: 'note',
        description: 'Service Workers are compatible with bfcache.',
      });
    }

    results.blockingReasons = issues;
    results.recommendations = [
      ...new Set(
        issues
          .filter((i) => i.severity !== 'info')
          .map((i) => i.description)
      ),
    ];

    if (issues.some((i) => i.source === 'browser')) {
      results.eligibility = 'blocked';
    } else if (issues.some((i) => i.source === 'detected')) {
      results.eligibility = 'likely-blocked';
    } else {
      results.eligibility = 'no-blockers-detected';
    }

    return results.eligibility;
  };

  const printReasons = (node, indent) => {
    const pad = ' '.repeat(indent);
    node.reasons.forEach((reason) => {
      console.log(`${pad}• ${reason}`);
      const help = REASON_HELP[reason.toLowerCase()];
      if (help) console.log(`${pad}  💡 ${help}`);
    });
    node.children.forEach((child, i) => {
      console.log(`${pad}Frame: ${child.src || child.url || child.name || child.id || `iframe ${i + 1}`}`);
      printReasons(child, indent + 3);
    });
  };

  const displayResults = () => {
    const statusIcons = { 'no-blockers-detected': '🟢', 'likely-blocked': '🟠', blocked: '🔴' };
    const statusColors = { 'no-blockers-detected': '#22c55e', 'likely-blocked': '#fb923c', blocked: '#ef4444' };
    const statusText = {
      'no-blockers-detected': 'No blockers detected',
      'likely-blocked': 'Likely blocked',
      blocked: 'Blocked (reported by the browser)',
    };

    console.group(
      `%c${statusIcons[results.eligibility] || '⚪'} bfcache: ${statusText[results.eligibility] || 'Unknown'}`,
      `color: ${statusColors[results.eligibility] || '#6b7280'}; font-weight: bold; font-size: 14px;`
    );

    const navEntry = performance.getEntriesByType('navigation')[0];
    console.log('');
    console.log('%c📊 Status:', 'font-weight: bold;');
    if (results.wasRestored) {
      console.log('%c   ✅ This page was restored from bfcache', 'color: #22c55e;');
    } else if (navEntry?.type === 'back_forward') {
      console.log('   ℹ️  This page was loaded by a back/forward navigation, not restored from bfcache');
    } else {
      console.log('   ℹ️  This page was loaded normally. Restoration is reported by a pageshow event.');
    }
    if (navEntry) console.log(`   Navigation type: ${navEntry.type}`);

    if (results.blockingReasons.length > 0) {
      console.log('');
      console.log('%c🔍 Findings:', 'font-weight: bold;');
      console.table(
        results.blockingReasons.map((i) => ({
          Severity: i.severity.toUpperCase(),
          Finding: i.reason,
          Detail: i.description,
        }))
      );
    } else {
      console.log('');
      console.log('%c✅ No bfcache blockers detected', 'color: #22c55e; font-weight: bold;');
    }

    if (results.notRestoredReasons) {
      console.log('');
      console.group('%c🔬 NotRestoredReasons (reported by the browser)', 'font-weight: bold; color: #3b82f6;');
      if (results.notRestoredReasons.reasons.length === 0 && results.notRestoredReasons.children.length === 0) {
        console.log('   No reasons reported');
      }
      printReasons(results.notRestoredReasons, 3);
      console.groupEnd();
    }

    console.log('');
    console.log(
      '%cNot every blocker is detectable from JavaScript: unload listeners added with addEventListener and open WebSocket, BroadcastChannel or IndexedDB connections only show up in NotRestoredReasons after a back navigation, or in DevTools → Application → Back/forward cache.',
      'color: #6b7280;'
    );

    if (results.recommendations.length > 0) {
      console.log('');
      console.log('%c💡 Recommendations:', 'color: #3b82f6; font-weight: bold;');
      results.recommendations.forEach((rec, idx) => console.log(`   ${idx + 1}. ${rec}`));
    }

    console.log('');
    console.log('%c🧪 How to test:', 'font-weight: bold;');
    console.log('   1. Navigate to another page');
    console.log('   2. Click the browser Back button');
    console.log('   3. Run this snippet again to read NotRestoredReasons, or use DevTools → Application → Back/forward cache');

    console.groupEnd();
  };

  const buildResult = () => ({
    script: 'Back-Forward-Cache',
    status: 'ok',
    details: {
      eligibility: results.eligibility,
      wasRestored: results.wasRestored,
      supported: results.supported,
      navigationType: performance.getEntriesByType('navigation')[0]?.type ?? null,
      notRestoredReasons: results.notRestoredReasons,
    },
    issues: results.blockingReasons.map((i) => ({
      severity: i.severity === 'high' ? 'error' : i.severity === 'medium' ? 'warning' : 'info',
      message: i.reason,
    })),
  });

  // Expose function for manual check
  window.checkBfcache = async () => {
    await analyze();
    displayResults();
    return buildResult();
  };

  console.log('%c🚀 bfcache Analysis', 'font-weight: bold; font-size: 14px;');
  console.log(
    '   Call %ccheckBfcache()%c anytime to re-run analysis.',
    'font-family: monospace; background: #f3f4f6; padding: 2px 4px;',
    ''
  );

  await analyze();
  displayResults();
  return {
    ...buildResult(),
    message: 'bfcache analysis complete. Call checkBfcache() to re-run analysis.',
  };
})();

Understanding bfcache

What is bfcache

The back/forward cache (bfcache) stores a complete snapshot of a page when you navigate away. When you press the browser back button, the page is restored instantly from memory instead of reloading.

Benefits

  • 0ms navigation: Instant page restoration
  • Preserved scroll position: Users return to exact scroll position
  • Preserved form state: Form inputs maintain their values
  • Preserved JavaScript state: Variables, timers, everything intact

When bfcache works

User flow with bfcache:
Page A → Page B → [Back] → Page A (instant, 0ms)

User flow without bfcache:
Page A → Page B → [Back] → Page A (full reload, 1-3s)

Common blockers and solutions

BlockerWhy it blocksSolution
unload eventCan't be reliably replayedUse pagehide or visibilitychange
beforeunloadPrevents navigation cachingRemove or use pagehide for cleanup
Cache-Control: no-storeTells browser not to cacheChange to no-cache or private
Open IndexedDB transactionState can't be serializedClose transactions on pagehide
WebSocket connectionCan't be suspendedClose on pagehide, reconnect on pageshow
Ongoing fetch/XHRIncomplete network stateAbort on pagehide
BroadcastChannelCan't be suspendedClose on pagehide

What the snippet detects

Several blockers are invisible to JavaScript, so the snippet separates what it can verify from what only the browser knows:

SourceWhat it reportsResult
NotRestoredReasons (after a back navigation)Every reason the browser found, for the page and for each frameblocked
window.onunload set as a propertyAn unload handlerlikely-blocked
Cache-Control: no-store headerRead with a HEAD request to the current URLlikely-blocked
iframes, Service WorkerContext only, never a blocker by themselvesinfo

When none of these apply, the result is no-blockers-detected. That is not a guarantee: unload listeners added with addEventListener, and open WebSocket, BroadcastChannel or IndexedDB connections, cannot be detected from JavaScript. They appear in NotRestoredReasons after a back navigation, or in DevTools → Application → Back/forward cache.

Fixing common issues

Issue 1: unload/beforeunload handlers

Bad
window.addEventListener("unload", () => {
  saveData(); // Blocks bfcache
});
 
window.addEventListener("beforeunload", () => {
  return "Are you sure?"; // Blocks bfcache
});
Good
window.addEventListener("pagehide", (event) => {
  // event.persisted tells you if page goes to bfcache
  if (event.persisted) {
    // Page will be cached, do minimal cleanup
    console.log("Page going to bfcache");
  } else {
    // Page being discarded, do full cleanup
    saveData();
  }
});

Issue 2: Cache-Control: no-store

The header is part of the HTTP response of the page. A <meta http-equiv="Cache-Control"> tag is ignored by browsers, so it has no effect on bfcache either way.

Bad
Cache-Control: no-store
Good
Cache-Control: no-cache

Keep no-store only for responses that contain sensitive data, and use private or no-cache for the rest.

Issue 3: Open WebSocket

Bad
const ws = new WebSocket("wss://web.dev");
// Connection stays open, blocks bfcache
Good
const ws = new WebSocket("wss://web.dev");
 
window.addEventListener("pagehide", () => {
  ws.close(); // Close on navigation
});
 
window.addEventListener("pageshow", (event) => {
  if (event.persisted) {
    // Reconnect after bfcache restore
    ws = new WebSocket("wss://web.dev");
  }
});

Issue 4: IndexedDB

Bad
const request = indexedDB.open("myDB");
request.onsuccess = () => {
  const db = request.result;
  const transaction = db.transaction(["store"], "readwrite");
  // Transaction stays open, blocks bfcache
};
Good
let db;
const request = indexedDB.open("myDB");
request.onsuccess = () => {
  db = request.result;
};
 
window.addEventListener("pagehide", () => {
  db?.close(); // Close connection
});

Testing bfcache

Method 1: Manual test (recommended)

  1. Open your page and run the snippet. It reports the blockers it can detect and starts listening for the pageshow event.
  2. Navigate to another page, for example by clicking a link.
  3. Press the browser Back button.
  4. Run the snippet again. The browser now reports NotRestoredReasons for the page and its frames.

A pageshow event with persisted: true is the only reliable sign that a page was restored. The snippet logs it as soon as it happens, but only if it was running before the navigation.

Output when the page is restored

⚡ Page restored from bfcache!

Method 2: Chrome DevTools

  1. Open DevTools → Application tab
  2. Click "Back/forward cache" in sidebar
  3. Click "Test back/forward cache"
  4. DevTools will navigate forward then back
  5. See detailed blocking reasons

NotRestoredReasons API (Chrome 123+)

Chrome 123+ exposes notRestoredReasons on the navigation entry. After a back or forward navigation that was not served from bfcache, it lists why. This is the most accurate way to diagnose bfcache issues.

API structure

const navEntry = performance.getEntriesByType("navigation")[0];
const nrr = navEntry.notRestoredReasons;
 
if (nrr) {
  console.log("URL:", nrr.url); // page URL
  nrr.reasons.forEach((detail) => {
    console.log("Reason:", detail.reason); // e.g. "unload-listener"
  });
 
  // Embedded frames, each with the same shape
  nrr.children.forEach((child) => {
    console.log("Frame:", child.src, child.reasons.map((d) => d.reason));
  });
}

Each entry of reasons is an object whose reason property holds the reason. The notRestoredReasons property is null when the page was restored, when the navigation was not a back or forward one, and in browsers without support.

The snippet walks the frame tree and returns it in details.notRestoredReasons, with one error issue for every reason and the frame it comes from.

Reason values

Common reason values

ReasonWhat it means
unload-listenerThe page or a frame has an unload event listener
response-cache-control-no-storeThe page response has Cache-Control: no-store
websocketOpen WebSocket connection
broadcastchannelOpen BroadcastChannel
indexeddb-connectionOpen IndexedDB connection
related-active-contentsA related window (popup, opener) blocks caching
maskedThe browser hides the exact reason, for example for a cross-origin frame

Example with an embedded frame

{
  "url": "https://example.com/",
  "reasons": [{ "reason": "masked" }, { "reason": "unload-listener" }],
  "children": [
    { "src": "/frame", "reasons": [{ "reason": "unload-listener" }], "children": [] }
  ]
}

This tells us:

  • The page was not restored from bfcache
  • The page has an unload listener
  • An embedded frame also has an unload listener, and it blocks the whole page
  • masked means the browser does not disclose one of the reasons
  • Action: replace both unload listeners with pagehide

Interpreting your results

ResultMeaningNext step
blockedThe browser reported reasons after a back navigationFix each listed reason, in the page and in its frames
likely-blockedThe snippet found an unload handler or Cache-Control: no-storeRemove it and test a back navigation to confirm
no-blockers-detectedNothing detectable was foundTest a back navigation; blockers that JavaScript cannot see only appear in NotRestoredReasons

Testing workflow

notRestoredReasons describes the navigation that just happened, so a fix is only confirmed by a new cycle:

  1. Open the page and navigate away. The browser decides at this point whether the page goes into bfcache.
  2. Press Back and run the snippet. Read the reasons.
  3. Fix the blockers: remove unload listeners, close connections on pagehide.
  4. Repeat from step 1. A pageshow event with persisted: true confirms the fix.

Chrome DevTools shortcut

  • DevTools → Application → Back/forward cache
  • Click "Test back/forward cache"
  • The panel lists the blocking reasons immediately

Browser support

The snippet needs these features to run.

FeatureChromeEdgeFirefoxSafari
PerformanceNavigationTiming (opens in a new tab) 57 12 58 15
PageTransitionEvent.persisted (opens in a new tab) 4 12 11 5
All of the above 57 12 58 15

The snippet reports on these features. It runs without them, and its advice on each one applies where it is supported.

FeatureChromeEdgeFirefoxSafari
PerformanceNavigationTiming.notRestoredReasons (opens in a new tab) 125 125

Source: MDN browser compatibility data (opens in a new tab), version 8.1.4.

All modern browsers support bfcache, but diagnostics vary.

Example output

Page with an unload handler, run before navigating away

🚀 bfcache Analysis
🟠 bfcache: Likely blocked

📊 Status:
   ℹ️  This page was loaded normally. Restoration is reported by a pageshow event.
   Navigation type: navigate

🔍 Findings:
┌─────────┬──────────┬──────────────────────────────┬─────────────────────────────────────────────────────────────────┐
│ (index) │ Severity │ Finding                      │ Detail                                                          │
├─────────┼──────────┼──────────────────────────────┼─────────────────────────────────────────────────────────────────┤
│ 0       │ 'HIGH'   │ 'window.onunload handler set'│ 'unload event listeners block bfcache. Use pagehide or ...'     │
└─────────┴──────────┴──────────────────────────────┴─────────────────────────────────────────────────────────────────┘

💡 Recommendations:
   1. unload event listeners block bfcache. Use pagehide or visibilitychange instead.

Same page after navigating away and pressing back

The page has an unload listener and an iframe with its own unload listener.

🔴 bfcache: Blocked (reported by the browser)

📊 Status:
   ℹ️  This page was loaded by a back/forward navigation, not restored from bfcache
   Navigation type: back_forward

🔬 NotRestoredReasons (reported by the browser)
   • masked
     💡 The browser does not disclose the exact reason (for example, a cross-origin frame).
   • unload-listener
     💡 unload event listeners block bfcache. Use pagehide or visibilitychange instead.
   Frame: /frame
      • unload-listener
        💡 unload event listeners block bfcache. Use pagehide or visibilitychange instead.

The details.notRestoredReasons property of the returned object carries the same tree, and issues has one error per reason and frame.

Real-world impact

Case studies

  • Wikipedia: 40% faster back navigations with bfcache
  • E-commerce sites: 50% reduction in bounce rate from back button
  • News sites: 60% of back navigations became instant

Measuring impact

Count restorations with the pageshow event: a restore fires it with persisted: true, while a regular back navigation loads the page again and reports notRestoredReasons.

RUM integration

// Track bfcache restoration in RUM
window.addEventListener("pageshow", (event) => {
  if (event.persisted) {
    // Page restored from bfcache
    gtag("event", "bfcache_restoration", {
      navigation_type: "back_forward",
      duration: 0,
    });
  }
});
 
// Track why a back/forward navigation did not use bfcache
const navEntry = performance.getEntriesByType("navigation")[0];
if (navEntry && navEntry.notRestoredReasons) {
  const reasons = navEntry.notRestoredReasons.reasons || [];
 
  gtag("event", "bfcache_not_restored", {
    reasons: reasons.map((detail) => detail.reason).join(", "),
  });
}

Further reading