In a plain web page, how does your code tell that the document was restored from the browser's back/forward cache rather than freshly loaded, and what does the `persisted` property mean on the `pageshow` and `pagehide` events?
answer
- two transition events, one boolean
- startup events belong to construction only
- the flag flips meaning per event
- true on the way out means frozen
- pageshow persisted marks the restore
basics
~20 sListen for pageshow: it fires after an ordinary load and after a back/forward cache restore, and event.persisted === true marks the restore. On pagehide, persisted === true means the page is going into that cache rather than being discarded.
solid answer
~40 sBoth events are `PageTransitionEvent`s carrying a boolean `persisted`. `pageshow` fires every time the document becomes the active page — once after the normal load, and again on every back/forward cache restore. `event.persisted` is `false` for the load and `true` for a restore, which is the only reliable in-page signal that nothing re-ran. `pagehide` fires when the document stops being the active page; there `persisted === true` means the browser is putting it into the cache, so it may come back alive, while `false` means it is being torn down for good. In practice: put anything that must repeat on every appearance — refreshing a timestamp, re-checking session state, restarting a poll — behind `pageshow` with `persisted`, and use `pagehide` to release things that would otherwise hold state open across the freeze.
code
javascript · 19 linesconst clock = document.createElement('span');
document.body.append(clock);
function render() {
clock.textContent = new Date().toLocaleTimeString();
}
let timer = 0;
window.addEventListener('pageshow', (event) => {
render();
timer = window.setInterval(render, 1000);
console.log(event.persisted ? 'restored from bfcache' : 'fresh load');
});
window.addEventListener('pagehide', (event) => {
window.clearInterval(timer);
console.log(event.persisted ? 'entering bfcache' : 'being discarded');
});go deeper
Know the name of the event to listen for: pageshow, with event.persisted telling you whether this was a restore. That single fact answers most screening versions of this question.
Be able to state the flag's meaning on both events — restored-from on pageshow, going-into on pagehide — and explain why load, popstate and visibilitychange are all the wrong signal.
Show judgment about what belongs in each handler: stop subscriptions and timers on pagehide, and on restore refresh only what decays — times, server-side data, session validity — rather than re-running initialisation and double-registering everything.
Frame this as a lifecycle contract the codebase must state once: where teams put start-up work, which state is allowed to survive a freeze, and how that is enforced so restores do not become a second, untested code path.
## Two events, one flag The back/forward cache is invisible to a page unless the page looks for it, because a restore fires none of the usual startup events. `DOMContentLoaded` and `load` belong to *document construction*, and a restored document was never re-constructed. The pair of events that do fire are: - **`pageshow`** — dispatched on `window` when the document becomes the active page. It fires immediately after `load` on a normal navigation, and again on every restore from the back/forward cache. - **`pagehide`** — dispatched on `window` when the document stops being the active page, whether it is about to be frozen or destroyed. Both receive a `PageTransitionEvent`, whose one interesting member is the boolean `persisted`. ## Reading `persisted` correctly The flag means different things on the two events, and mixing them up is the classic mistake: | Event | `persisted === true` | `persisted === false` | | --- | --- | --- | | `pageshow` | the document came **out of** the back/forward cache | this is an ordinary first load | | `pagehide` | the document is going **into** the back/forward cache | the document is being discarded | So on the way in, `true` means "you were frozen and are now thawed"; on the way out, `true` means "you are about to be frozen, and might be thawed later". ```js window.addEventListener('pageshow', (event) => { if (event.persisted) { // nothing re-ran: no load, no DOMContentLoaded, no module evaluation revalidateSession(); restartPolling(); } }); window.addEventListener('pagehide', (event) => { stopPolling(); if (!event.persisted) { releaseEverything(); // this document is not coming back } }); ``` ## Why `pageshow` and not something else Candidates often reach for the wrong signal: - `load` / `DOMContentLoaded` — fire once per constructed document, never on a restore. - `popstate` — fires for *same-document* history traversals, the case where the document never changed. A bfcache restore brings back a whole different document, so `popstate` is not the signal you want. - `visibilitychange` — tells you the tab became visible again, which happens for tab switching too and is not specific to a cache restore. - A `PerformanceNavigationTiming` entry with `type === 'back_forward'` — tells you the navigation *was* a history traversal, but a traversal can also be an ordinary load if the page was ineligible or evicted. It is not a restore flag. Only `pageshow` with `persisted` says "this exact document was frozen and is now running again". ## What belongs in the handler The restore handler is not a second `main()`. Nothing was destroyed, so re-initialising everything would double-register listeners and duplicate work. What genuinely belongs there is anything whose value decays while the page is frozen: - displayed times, countdowns and "x minutes ago" labels, - data that another tab or the server may have changed, - session or permission state that could have been invalidated, - polls, subscriptions and connections you deliberately stopped in `pagehide`. The symmetric point matters: `pagehide` is where you *stop* those things, because while the page is frozen it cannot run any code to stop them itself, and holding some of them open can even cost the page its eligibility. ## Firing order and reliability On the way out, the browser marks the tab hidden and then fires `pagehide`; on the way back in, `pageshow` fires and the page becomes visible again. `pagehide` fires for both the freeze and the destroy path, which is what makes it a usable pairing with `pageshow` — you get a matched "leaving" and "arriving" pair regardless of which path the browser takes. A final subtlety: `pagehide` with `persisted === true` means the browser *intends* to cache the page, not that the page will definitely be restored. The entry can be evicted for memory or age, and the user may never go back at all. Treat it as "maybe frozen" and keep the discard path correct too.
- Why is `load` a poor place to put code that must run every time the page appears?Because `load` belongs to document construction and a restored document was never re-constructed — it fires exactly once in the page's lifetime. Anything registered only there is silently skipped on every back/forward restore, which is how clocks freeze at an old time and polling never resumes after a Back.
- If `pagehide` fires with `persisted === true`, is the page guaranteed to come back?No. It only means the browser is putting the document into the back/forward cache. The entry can be evicted under memory pressure or after the browser's age limit, and the user may never navigate back. Code must stay correct if that document is simply never resumed.
- Would `document.visibilityState` or a `visibilitychange` listener work as a restore signal?Not on its own. Visibility changes on tab switches, window minimising and backgrounding too, so it cannot distinguish "this frozen document was restored" from "the user came back to a tab that never stopped running". `pageshow` with `event.persisted` is the specific signal.
saying these in an interview costs you the question
- Expects DOMContentLoaded or load to fire again on restore
- Reads pagehide's persisted as meaning the page was restored
- Uses popstate as the bfcache restore signal
- Re-runs full initialisation inside the pageshow handler
- Assumes a cached entry will definitely be restored later