skip to content

Field data shows most Back navigations to your site are full page loads rather than back/forward cache restores. How do you find out, page by page, why the browser refused to restore each one?

level: seniorimportance: nice to knowfreq 28%

answer

  1. you cannot guess, you must attribute
  2. one panel tests it by really traversing
  3. traversal type is not the outcome
  4. navigation timing carries the reasons tree
  5. gate it in CI or it regresses

basics

~20 s

Reproduce it locally in Chrome DevTools' Application panel, which has a Back/forward cache tool that navigates away and back and lists the blocking reasons. For real users, read PerformanceNavigationTiming.notRestoredReasons, available in Chrome 123 and later.

solid answer

~50 s

Two complementary sources. **Locally**, Chrome DevTools' Application panel includes a Back/forward cache tool with a test button that performs a real navigation away and back, then reports whether the page was restored and, if not, the specific blocking reasons — an `unload` handler, a `no-store` document response, an open connection, or a frame that is itself ineligible. That is the fastest way to attribute a failure to a line of code. **In the field**, read the navigation timing entry: `performance.getEntriesByType('navigation')[0]` has `type === 'back_forward'` for a history traversal, and in Chrome 123 and later a `notRestoredReasons` property carrying the reasons tree, including per-frame entries. Report that alongside your usual analytics and you get an eligibility rate per URL plus the ranked causes. In-page, `pageshow` with `event.persisted` gives you the coarse restored/not-restored signal on any browser.

code

javascript · 12 lines
javascript
const [nav] = performance.getEntriesByType('navigation');

if (nav && nav.type === 'back_forward') {
  // Chromium 123+ only; feature-detect before reading
  if ('notRestoredReasons' in nav && nav.notRestoredReasons) {
    console.log('not restored:', nav.notRestoredReasons);
  }
}

window.addEventListener('pageshow', (event) => {
  console.log(event.persisted ? 'bfcache restore' : 'full load');
});

go deeper

for a junior

Know where to look first: Chrome DevTools' Application panel has a Back/forward cache tool whose test button navigates away and back and reports why the page was not restored.

for a middle

Explain the difference between the signals — pageshow's event.persisted gives the outcome, navigation timing's type === 'back_forward' only says it was a traversal, and notRestoredReasons gives the cause on Chromium 123+.

for a senior

Show the whole loop: reproduce and attribute locally, instrument real users to rank causes per URL, then map each reason to its fix — move unload work to pagehide, narrow no-store, tear down connections, chase blocking frames.

for a principal

Treat eligibility as a guarded property rather than a one-off cleanup: decide who owns the metric, gate it in CI so a new third-party tag cannot silently disable it, and weigh that ongoing cost against the pages where no-store is non-negotiable.

## Why you cannot guess Eligibility is decided by the whole page — your bundle, every third-party tag, every cross-origin frame, and the document's own response headers. A page can look clean in your source and still never be restored because one vendor script registers an `unload` listener, or because a CDN rule adds `Cache-Control: no-store`. You need attribution, not intuition. ## The local tool: DevTools Chrome DevTools' **Application** panel has a **Back/forward cache** tool. Its test button drives a real navigation away from the current page and straight back, then reports one of two outcomes: the page was successfully served from the back/forward cache, or it was not — with a list of reasons. The reasons are specific enough to act on, and they are grouped by whether they are actionable in your code, browser-internal circumstances, or not supported yet. The crucial detail is that the tool performs an actual traversal. You cannot reproduce a restore by reloading, retyping the URL or clicking a link — those are new navigations. Any manual test must use Back/Forward, `history.back()`, or this tool. ## The field signal: navigation timing For real users, the browser exposes the outcome on the navigation timing entry: ```js const [nav] = performance.getEntriesByType('navigation'); if (nav.type === 'back_forward') { // this was a history traversal; it may or may not have been a restore console.log(nav.notRestoredReasons); } ``` `type === 'back_forward'` tells you the navigation was a history traversal. It does **not** tell you a restore happened — a traversal to an ineligible or evicted page is an ordinary load with that same type, which is precisely the population you are investigating. `PerformanceNavigationTiming.notRestoredReasons`, available in Chrome 123 and later, closes that gap: on a traversal that was not restored, it carries a structured report of why, with a tree that mirrors the frame hierarchy so a blocking cross-origin iframe can be attributed rather than guessed at. Feature-detect it (`'notRestoredReasons' in nav`) because it is Chromium-only, and expect the reason strings to change as browsers change behaviour — treat them as a diagnostic dimension, not a stable contract. ## The universal in-page signal On any browser, `pageshow` with `event.persisted` gives you the binary outcome: ```js window.addEventListener('pageshow', (event) => { reportMetric('bfcache_restore', event.persisted ? 1 : 0); }); ``` Combine the two and you get a usable funnel: how many navigations are traversals, what share of traversals were restores, and for the rest, the ranked reasons. ## Turning data into fixes The reasons cluster into a small number of causes with known remedies. An `unload` listener anywhere in the page — audit your own bundle and every third-party tag, then move the work to `pagehide`. `Cache-Control: no-store` on the document — check whether it was intended for this route or applied globally by a CDN rule, and narrow it. Open or in-flight connections at navigation time — close sockets and stop long-running work in `pagehide`, and restart in the `pageshow` restore branch. A blocking cross-origin frame — take it to the vendor, or load the frame lazily so it is not present at navigation time. ## Guarding the win Eligibility is a regression-prone property: it is invisible in normal QA, it is binary, and one added tag turns it off. Teams that care about it wire the check into CI — a headless run that navigates away and back and asserts a restore — and keep the field eligibility rate on the same dashboard as their other page metrics. Being able to describe that loop, rather than only naming the DevTools panel, is what separates a senior answer here.

  • Why is `PerformanceNavigationTiming.type === 'back_forward'` not enough on its own?
    It only says the navigation was a history traversal. A traversal to a page that was ineligible or already evicted is an ordinary load and still reports `back_forward` — which is exactly the failing population you are investigating. Pair it with `pageshow`'s `event.persisted` for the outcome, or `notRestoredReasons` for the cause.
  • Why can't you reproduce a restore by reloading the page or re-entering its URL?
    Because the back/forward cache serves history traversals only. A reload, a typed URL, a bookmark or a link click all create new navigations and construct a fresh document. Any manual or automated test has to go forward to another page and then use Back, `history.back()`, or the DevTools test button.
  • How would you stop eligibility from silently regressing after you have fixed it?
    Automate the traversal check: in CI, navigate away from the page and back with a headless browser and assert the restore, so a newly added tag or header fails the build. Pair that with a field metric — the share of traversals reporting `event.persisted` — on the same dashboard as your other page metrics.

saying these in an interview costs you the question

  • Treats navigation type back_forward as proof of a restore
  • Tries to reproduce a restore by reloading the page
  • Reads notRestoredReasons without feature-detecting it
  • Assumes only first-party code can block eligibility
  • Checks eligibility once and never guards against regressions

context