How to Restore and Refresh State on pageshow After bfcache

This guide completes the work in Back/Forward Cache (bfcache), within Advanced Caching Strategies & CDN Architecture. A page restored from bfcache resumes exactly as it was when the user left: the same DOM, the same JavaScript state, the same cart count and the same "only 2 left" stock badge. That is the point — it is instant — but some of that state may now be wrong. The user added an item in another tab; the price changed; the session expired; a countdown timer was paused while frozen.

The fix is to treat restoration as a resume event: listen for pageshow with event.persisted === true, and refresh exactly the state that can go stale. Done well, users get an instant page and correct data a moment later. Done badly — or not at all — teams sometimes "fix" stale-data bugs by making pages ineligible for bfcache, throwing away the performance win.

Restore, then refresh Sequence of a bfcache restoration followed by targeted refreshes of stale state. Restore, then refresh Browser Page API pageshow (persisted) GET /cart/summary 3 items badge updated

Rapid Diagnosis

  • Navigate away and back on key templates after changing state elsewhere (add to cart in another tab, log out, wait past a timer). Note anything stale.
  • Check timers and intervals. They pause while frozen and may fire immediately on restore.
  • Check connections. WebSockets closed on pagehide must reopen on pageshow.
  • Check analytics. Restored pages should register a view; some tools miss them.

Root Cause Analysis

1. Cross-tab changes. State changed in another tab (cart, preferences, auth) is not reflected in the frozen page.

2. Time-based state. Countdown timers, "time ago" labels, and expiring offers show values from when the page was frozen.

3. Server-side changes. Prices, stock levels and notifications may have changed.

4. Paused activity. Polling and connections stopped on pagehide are not restarted.

What to refresh on restore Categories of page state, whether they go stale while a page sits in bfcache, and how to refresh each. What to refresh on restore State Goes stale? Refresh by Cart count / badge yes, cross-tab fetch summary or BroadcastChannel Prices and stock yes, server-side refetch visible items Session / auth yes check session endpoint Scroll position and form input no — keep it nothing Timers and labels time-based recompute from timestamps

Step-by-Step Resolution

1. Centralise restore handling

javascript
const onRestore = new Set();
export const whenRestored = (fn) => onRestore.add(fn);
addEventListener('pageshow', (event) => {
  if (event.persisted) for (const fn of onRestore) fn();
});
// trade-off: a central registry makes it easy to add work on restore; keep each
// handler cheap and async so restoration stays instant.

Expected outcome: features register their own refresh logic in one consistent way.

2. Refresh cross-tab and server state

javascript
whenRestored(async () => {
  const summary = await fetch('/api/cart/summary', { cache: 'no-store' }).then((r) => r.json());
  cartBadge.textContent = summary.count;
});
// trade-off: a request per restore adds server load; for state that changes in
// other tabs only, BroadcastChannel or storage events can update without a request.

Expected outcome: badges and counts are correct within a few hundred milliseconds of restoration.

3. Recompute time-based values

Store absolute timestamps and recompute labels and countdowns from them, rather than decrementing counters in intervals that pause while frozen.

javascript
whenRestored(() => document.querySelectorAll('time[data-relative]').forEach((el) => {
  el.textContent = formatRelative(new Date(el.dateTime));
}));
// trade-off: recomputing many labels at once can cost a few milliseconds on
// long pages; limit it to elements in or near the viewport if needed.

4. Re-establish connections and polling

Reopen WebSockets and restart polling that was stopped on pagehide, then fetch any changes missed while frozen.

5. Handle expired sessions

Check the session on restore for authenticated pages and redirect to login if it has ended — the pattern from Cache-Control no-store and bfcache.

Restore handler checklist Five things a page should do when restored from bfcache. Restore handler checklist Keep scroll and form state That is the user's context — do not reset it Refresh cross-tab state Cart, wishlist, notifications Refetch volatile server data in view Prices and stock for visible items Recompute time-based UI From stored timestamps Reconnect and check session Sockets, polling, auth 1 2 3 4 5

Verification

Script the scenario in an end-to-end test: open a product list, navigate to a product, add to cart in a second tab, go back in the first tab, and assert the badge updates. Confirm the restore remains instant (no full reload) by checking pageshow.persisted. Monitor error rates for restore handlers in production.

Worked Example: Restoring Without Losing Instant Back

An online grocery site had disabled bfcache with an unload handler after customers complained that the basket count was wrong after going back. The team removed the handler, added a restore registry, refreshed the basket summary and visible product availability on pageshow, and recomputed delivery slot countdowns from timestamps. Back navigations became instant for 80% of users in Chromium, and complaints about stale baskets stopped because the count now updated within 200ms of returning.

Common Mistakes

  • Reloading the page on restore. location.reload() in pageshow throws away the benefit entirely.
  • Refreshing everything. Refetch only what can go stale; re-rendering the whole page negates the instant restore.
  • Resetting scroll or form state. Users expect to return to where they were.
  • Blocking on refresh. Show the restored page immediately; update asynchronously.

Edge Cases

Optimistic UI. Pending optimistic updates (an item marked "adding…") may have completed or failed while frozen; reconcile with server state on restore.

Single-page apps with client caches. Data libraries like TanStack Query can refetch on window focus; ensure that behaviour also triggers on restore, or call invalidateQueries from the restore handler.

Analytics. Send a page view on restore with the navigation type so restored views are counted.

Feature flags. If flags can change mid-session, decide whether a restored page should pick up new flags; usually it should not change layout under the user.

Framework Integration

Most frameworks do not handle restoration for you, but their data layers make it straightforward. In React, a small hook that subscribes to pageshow and calls your data library's refetch (queryClient.invalidateQueries, SWR's mutate) refreshes visible queries. In Vue, a composable can do the same with Pinia stores or refreshNuxtData() in Nuxt. In both, keep the restore logic declarative: components declare which data is volatile, and a single listener refreshes those queries on restore, rather than every component adding its own pageshow handler. That keeps the restoration path cheap and predictable as the application grows.

FAQ

Does pageshow fire on normal page loads too?

Yes, with persisted false. Check event.persisted to run restore-only logic.

Is visibilitychange enough instead of pageshow?

visibilitychange fires on tab switches too, which is useful for refreshing data when a user returns to a tab, but it does not tell you the page was restored from bfcache. Use pageshow for restore-specific logic and visibilitychange for general freshness.

Will timers fire immediately after restore?

Timers pause while frozen; when the page resumes, any timer whose time has passed may fire promptly. Code that assumes regular intervals should compute from timestamps rather than counting ticks.

How quickly should refreshed data appear?

Within a few hundred milliseconds is typical and acceptable; the page itself is already visible and interactive. Indicate pending refreshes subtly for important values like prices.

Can BroadcastChannel keep tabs in sync instead?

Yes, for cross-tab changes: a tab that adds to the cart can broadcast the new count, and other tabs update on receipt or on restore. Close channels on pagehide if your browser targets treat open channels as blockers.

Should restored pages re-run A/B test assignment?

No. The user already saw a variant; changing it on restore would alter the page under them and corrupt experiment data. Keep the original assignment and only refresh data that can legitimately change, such as prices or counts.