# Back/forward cache (bfcache)

> Source: <https://webperf-snippets.net/Loading/Back-Forward-Cache>

Analyzes [Back/Forward Cache (bfcache)](https://web.dev/articles/bfcache) 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 Type     | Load Time | User Experience |
| ------------------- | --------- | --------------- |
| Normal navigation   | 1-3s      | Wait, spinner   |
| bfcache restoration | 0ms       | Instant         |

### How bfcache works

Diagram: 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 full

### Common blocking reasons

Diagram: 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 reloaded

### See what a blocker costs

Turn features of Page A on and off, then press the back button to compare an instant restore with a full reload. The table lists the reason the browser reports and whether the snippet can see it beforehand:

Interactive demo: [Interactive back navigation that compares an instant bfcache restore with a full reload, with toggles for an unload listener, Cache-Control no-store, an open WebSocket, an open IndexedDB connection and a cross-origin iframe, and the NotRestoredReasons each one produces](https://webperf-snippets.net/demos/bfcache-blockers.html). Toggle the features of Page A and press the back button to see the restore or the reload.

### Snippet

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

(async () => {
  // @shared ownRequestUrl
  // The URL a snippet requests when it reads a file of the page itself (fetch, a HEAD request,
  // new Image()). The fragment never reaches the server or the HTTP cache key, so the file still
  // comes from the cache, but it stays in the name of the Resource Timing entry of the request,
  // which is how isOwnRequest leaves these requests out of what the page loaded.
  function ownRequestUrl(url) {
    try {
      const parsed = new URL(url, location.href);
      if (!/^https?:$/.test(parsed.protocol)) return url;
      parsed.hash = "webperf-snippets";
      return parsed.href;
    } catch {
      return url;
    }
  }
  // @end-shared ownRequestUrl

  // 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(ownRequestUrl(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

| Blocker                        | Why it blocks               | Solution                                     |
| ------------------------------ | --------------------------- | -------------------------------------------- |
| **unload event**               | Can't be reliably replayed  | Use `pagehide` or `visibilitychange`         |
| **beforeunload**               | Prevents navigation caching | Remove or use `pagehide` for cleanup         |
| **Cache-Control: no-store**    | Tells browser not to cache  | Change to `no-cache` or `private`            |
| **Open IndexedDB transaction** | State can't be serialized   | Close transactions on `pagehide`             |
| **WebSocket connection**       | Can't be suspended          | Close on `pagehide`, reconnect on `pageshow` |
| **Ongoing fetch/XHR**          | Incomplete network state    | Abort on `pagehide`                          |
| **BroadcastChannel**           | Can't be suspended          | Close 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:

| Source                                         | What it reports                                                 | Result           |
| ---------------------------------------------- | --------------------------------------------------------------- | ---------------- |
| `NotRestoredReasons` (after a back navigation) | Every reason the browser found, for the page and for each frame | `blocked`        |
| `window.onunload` set as a property            | An `unload` handler                                             | `likely-blocked` |
| `Cache-Control: no-store` header               | Read with a `HEAD` request to the current URL                   | `likely-blocked` |
| iframes, Service Worker                        | Context only, never a blocker by themselves                     | `info`           |

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

```javascript
window.addEventListener("unload", () => {
  saveData(); // Blocks bfcache
});

window.addEventListener("beforeunload", () => {
  return "Are you sure?"; // Blocks bfcache
});
```

##### Good

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

```http
Cache-Control: no-store
```

##### Good

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

```javascript
const ws = new WebSocket("wss://web.dev");
// Connection stays open, blocks bfcache
```

##### Good

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

```javascript
const request = indexedDB.open("myDB");
request.onsuccess = () => {
  const db = request.result;
  const transaction = db.transaction(["store"], "readwrite");
  // Transaction stays open, blocks bfcache
};
```

##### Good

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

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

| Reason                            | What it means                                                            |
| --------------------------------- | ------------------------------------------------------------------------ |
| `unload-listener`                 | The page or a frame has an `unload` event listener                       |
| `response-cache-control-no-store` | The page response has `Cache-Control: no-store`                          |
| `websocket`                       | Open WebSocket connection                                                |
| `broadcastchannel`                | Open BroadcastChannel                                                    |
| `indexeddb-connection`            | Open IndexedDB connection                                                |
| `related-active-contents`         | A related window (popup, opener) blocks caching                          |
| `masked`                          | The browser hides the exact reason, for example for a cross-origin frame |

### Example with an embedded frame

```json
{
  "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

| Result                 | Meaning                                                            | Next step                                                                                       |
| ---------------------- | ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- |
| `blocked`              | The browser reported reasons after a back navigation               | Fix each listed reason, in the page and in its frames                                           |
| `likely-blocked`       | The snippet found an `unload` handler or `Cache-Control: no-store` | Remove it and test a back navigation to confirm                                                 |
| `no-blockers-detected` | Nothing detectable was found                                       | Test 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.

| Feature                                                                                                   | Chrome | Edge | Firefox | Safari |
| --------------------------------------------------------------------------------------------------------- | :----: | :--: | :-----: | :----: |
| [PerformanceNavigationTiming](https://developer.mozilla.org/docs/Web/API/PerformanceNavigationTiming)     |  ✅ 57  | ✅ 12 |   ✅ 58  |  ✅ 15  |
| [PageTransitionEvent.persisted](https://developer.mozilla.org/docs/Web/API/PageTransitionEvent/persisted) |   ✅ 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.

| Feature                                                                                                                                     | Chrome |  Edge | Firefox | Safari |
| ------------------------------------------------------------------------------------------------------------------------------------------- | :----: | :---: | :-----: | :----: |
| [PerformanceNavigationTiming.notRestoredReasons](https://developer.mozilla.org/docs/Web/API/PerformanceNavigationTiming/notRestoredReasons) |  ✅ 125 | ✅ 125 |    ❌    |    ❌   |

Source: [MDN browser compatibility data](https://github.com/mdn/browser-compat-data), 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

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

- [Back/forward cache (bfcache)](https://web.dev/articles/bfcache) | web.dev
- [NotRestoredReasons API](https://developer.chrome.com/docs/web-platform/bfcache-notrestoredreasons/) | Chrome Developers
- [Page Lifecycle API](https://developer.chrome.com/docs/web-platform/page-lifecycle-api/) | Chrome Developers
- [bfcache tester](https://back-forward-cache-tester.glitch.me/) | Interactive testing tool
