skip to content

A user clicks Log out, lands on the login page, then presses Back — and the dashboard reappears fully rendered with their name and data, restored from the back/forward cache. Why does the frozen page come back intact, and what are your options for handling state that has gone stale while a page sat in that cache?

level: seniorimportance: should knowfreq 38%

answer

  1. the page never heard about the logout
  2. frozen documents run no code
  3. two levers: ineligible, or re-validate
  4. the restore paints before your check returns
  5. scope the header to sensitive views

basics

~20 s

A restored page resumes the exact heap and DOM it had when frozen, so already-rendered data reappears without any code running to re-check it. Fix it by serving the sensitive document with Cache-Control: no-store, or by re-validating on pageshow when event.persisted is true.

solid answer

~50 s

The back/forward cache restores a paused document, not a fresh one: no request, no script evaluation, no framework mount. Whatever was on screen and in memory when the user navigated away comes back byte-for-byte, including a rendered dashboard whose session has since been invalidated server-side. Nothing about logging out reaches that frozen page, because a frozen page runs no code. You have two levers. The blunt one is to make the document ineligible — send `Cache-Control: no-store` on authenticated HTML — which forces a real navigation on Back and is the right call for genuinely sensitive views. The precise one is to keep eligibility and re-validate on restore: in a `pageshow` handler, when `event.persisted` is true, re-check session state and either refresh the data or navigate away. The same handler is where you refresh anything else time-decayed — timestamps, counters, prices — and re-open connections you closed in `pagehide`.

code

javascript · 18 lines
javascript
const sensitive = document.querySelector('#dashboard');

window.addEventListener('pageshow', (event) => {
  if (!event.persisted) return;

  // hide first, verify second: the restore paints before any fetch resolves
  if (sensitive) sensitive.hidden = true;

  fetch('/api/session', { credentials: 'include' })
    .then((res) => {
      if (!res.ok) {
        location.replace('/login');
        return;
      }
      if (sensitive) sensitive.hidden = false;
    })
    .catch(() => location.replace('/login'));
});

go deeper

for a junior

Understand that a restored page shows exactly what it showed before, including data that has since changed, because no code ran while it was frozen.

for a middle

Explain both levers and their mechanism: Cache-Control: no-store makes the document ineligible so Back re-navigates, while pageshow with event.persisted lets an eligible page re-validate itself on restore.

for a senior

Show the production judgment: pick per view based on what a stale render actually costs, hide sensitive regions synchronously before an async check, and pair pagehide teardown with pageshow restart for sockets and polls.

for a principal

Own the policy across the product — which classes of page may keep eligibility, what the standard restore contract is for every team, and how you avoid a blanket no-store that trades a site-wide performance win for a problem confined to a few views.

## Why the stale page appears at all A restore is not a load. The browser reinstates a document it had frozen: the DOM as last rendered, the JavaScript heap with every variable and closure, framework component state, scroll position. No script re-runs, so nothing in the page has an opportunity to notice that the world moved on. Log-out happened on the *server* and in the *next* document; the frozen dashboard was never told and could not have listened if it had been. This generalises past auth. Anything the page rendered from data is a snapshot of the moment it was frozen: prices, inventory counts, unread badges, "3 minutes ago" labels, a shopping cart, a permissions-derived UI. The page can sit frozen for minutes before the user comes back. ## Lever one: refuse eligibility If a view must never be shown again after the user leaves it, make it bfcache-ineligible by serving the document with `Cache-Control: no-store`. Back then performs a real navigation, the server sees the request, and an invalidated session produces a redirect to login instead of a restored dashboard. This is exactly why authenticated pages have carried `no-store` for years. The cost is real and should be stated in an interview: you lose instant Back on that page permanently, for every user, including the overwhelming majority whose session is still perfectly valid. So scope it — apply `no-store` to the views that genuinely carry sensitive rendered content, not as a global default that quietly disables the feature site-wide. ## Lever two: keep eligibility, re-validate on restore The alternative is to accept the restore and immediately correct it. The restore signal is `pageshow` with `event.persisted === true`: ```js window.addEventListener('pageshow', async (event) => { if (!event.persisted) return; const res = await fetch('/api/session', { credentials: 'include' }); if (!res.ok) { location.replace('/login'); return; } refreshDashboardData(); }); ``` This keeps the instant-Back win for the common case and costs one cheap request in the restore path. Its weakness is a visible window: the stale content is painted before the check resolves, so for content that must never be *seen*, this is not sufficient on its own — you would need to hide or blank the sensitive region synchronously at the top of the handler before awaiting. The blunt version of the same lever is `location.reload()` inside the `persisted` branch. It is honest and simple, and it throws away the entire benefit of the cache for that page, so it is a reasonable fallback and a poor default. ## What else goes stale While frozen, the document runs nothing: timers do not fire, no `fetch` completes, no message is processed, no observer callback runs. Consequently, after a restore: - displayed clocks and relative times are wrong by however long the freeze lasted, - polled data is as old as the last poll before the freeze, - a WebSocket you left open may have been closed underneath you, and any messages sent meanwhile are gone, - another tab may have changed shared storage, and the frozen page did not process the `storage` event, - feature flags, permissions and prices may all have moved. The symmetric pattern handles most of it: in `pagehide`, stop polls and close connections, because a frozen page cannot stop them itself; in `pageshow` with `persisted`, restart them and re-pull whatever they feed. ## Choosing between the levers Ask what the failure actually costs. A stale unread badge is a cosmetic bug — re-validate on restore. A stale price on a checkout page is a commercial bug — re-validate, and block interaction until the check returns. A rendered view of someone's medical or financial data after logout is a disclosure — make the document `no-store` and take the reload. ## The framing interviewers want The weak answer is "bfcache is dangerous, turn it off". The strong answer separates two things: the cache is a large, free performance win on most pages, and *rendered state outliving its validity* is a property your application must reason about anyway — a phone left on a page for an hour, a restored tab, a duplicated tab all raise the same question. bfcache just makes it happen far more often, and gives you one precise hook (`pageshow` + `persisted`) to respond to it.

  • Why is re-validating inside the `pageshow` handler not enough for content that must never be seen?
    Because the restore paints the frozen document before your asynchronous check resolves — the stale view is on screen for at least a network round trip. If exposure itself is the problem, hide the region synchronously at the top of the handler and reveal it only after validation, or make the document `Cache-Control: no-store` so it is never restored.
  • What happens to an open WebSocket while the page is frozen, and what should the page do about it?
    The page runs no code, so it cannot send, receive or respond to a close; browsers may tear the connection down on entry, and behaviour has been changing across releases. Treat the socket as gone: close it deliberately in `pagehide`, and re-open plus re-sync missed state in `pageshow` when `event.persisted` is true.
  • Is `location.reload()` in the restore handler an acceptable solution?
    It is correct but expensive: it discards the entire benefit of the restore for that page, on every Back, for every user. Use it as a quick fix or where the page cannot be selectively refreshed, and prefer a targeted re-validation — or `no-store` on genuinely sensitive documents — as the considered answer.
  • How would a frozen page miss a change another tab made to `localStorage`?
    A frozen document processes no tasks, so the `storage` event that would normally notify it is never handled. On restore it still holds whatever it read before the freeze. Re-read the values you depend on inside the `pageshow` `persisted` branch rather than trusting in-memory copies from before the freeze.

saying these in an interview costs you the question

  • Assumes logging out invalidates pages already frozen in memory
  • Says bfcache should simply be disabled everywhere for safety
  • Puts re-validation in a load handler that never fires again
  • Believes an async check in pageshow prevents the stale paint
  • Treats no-store as free rather than a permanent eligibility cost

context