bfcache restore problems — diagnose and design for correct back/forward restores

Symptom scanner

How to tell a bfcache restore from a full reload

Quick observable signs you can check now (console, network, UI).

  • Console: absence of the usual startup logs (no script re-run) suggests a restore; a full reload typically re-invokes module-level code and prints initialization messages.
  • Network: no network requests for initial bundles/resources during back/forward implies restore; a full reload will show requests for top-level assets in the network tab.
  • UI behavior: transient UI (open menus, scroll position, form contents) preserved on successful restore; if these reset, treat it as a reload or partial loss.
Eligibility checklist

Common page-side signals that opt pages out of bfcache

Inspect these signals in your codebase and runtime.

  • synchronous unload handlers (sync XHR or blocking operations during beforeunload/unload) — modern engines treat them as ineligible.
  • active platform handles such as open WebSocket references, dedicated workers, or unclosed BroadcastChannel instances.
  • non-passive or capturing touch listeners attached to document/window that block scroll or rely on synchronous side effects.
  • page-modifying service worker interactions that trigger fetch-driven updates during navigation checkpoints.
  • document.write or dynamic base changes executed on restore path in some contexts.
Lifecycle map

Navigation checkpoints: suspend → checkpoint → restore

Where to capture or rehydrate state during the bfcache lifecycle.

Before the browser suspends a page it runs certain lifecycle steps where you must avoid blocking operations. Treat the sequence as three reliable moments: capture ephemeral UI (scroll, open panels) at suspension, persist minimal checkpoints synchronously if needed, and rehydrate only the UI bits your app cannot reconstruct lazily during restore. Do not assume scripts will re-run on restore — plan for a fast reattachment path.

Common culprits

Timers, service workers, websockets, and synchronous unloads

Short vignettes explaining practical symptoms.

  • Timers: long-running intervals or animation loops that run on every page visit can mislead you into thinking a restore failed when handlers reattach. Use clear start/stop semantics and avoid relying on global timers without lifecycle checks.
  • Service workers: updates or strategy changes that trigger reloads can cause full navigations. Confirm whether service worker update flows are intentionally forcing reloads during back navigation.
  • WebSockets / RTC: open sockets are page-held resources. If the page leaves sockets open and the browser treats them as non-restartable, the page becomes ineligible; prefer reconnect-on-restore approaches.
  • Synchronous unloads: any blocking call in beforeunload/unload is a strong opt-out. Replace sync side-effects with asynchronous safe checkpoints or ephemeral heartbeat channels that can be resumed.
State taxonomy

Ephemeral UI, persisted state, and session checkpoints

Where to store what, and when to reconstruct.

Classify state so restoration becomes predictable. Ephemeral UI (scroll position, open accordions) belongs to in-memory checkpoints and should be captured at suspend and re-applied on restore. Long-lived state (user preferences) belongs in persistent storage (IndexedDB/local persistence) and be rehydrated lazily. Session checkpoints are minimal serializable snapshots you can write synchronously on navigation if needed — keep them tiny to avoid blocking eligibility.

Design patterns that play well

Resumable init, idempotent handlers, lazy reattachment, passive listeners

Concrete design approaches that reduce opt-out signals.

  • Resumable initialization: detect whether you are restoring and skip full bootstrap; run a lightweight reattach that verifies existing DOM and state before running heavy work.
  • Idempotent handlers: ensure event listener registration can be called safely multiple times or gated by presence checks.
  • Lazy reattachment: delay reconnecting expensive resources (WebSocket, worker) until after restore verification or user interaction, or gate with feature flags.
  • Passive listeners: mark touch/scroll listeners passive where possible and avoid capture where not required.
Diagnostic checklist

Step-by-step reproducible checks

Follow these steps in order to surface the likely cause.

  1. Reproduce with clean profile/devtools: open devtools, clear site data if needed, and perform the navigation sequence (visit → navigate away → back).
  2. Observe console at both pages: look for startup logs or missing logs that indicate whether scripts re-ran.
  3. Watch the network tab during back navigation: absence of asset requests strongly suggests a restore path.
  4. Temporarily disable suspected handlers (beforeunload, open sockets) and retry; re-enable one-by-one to isolate.
  5. Use small checkpoints: serialize minimal UI state on checkpoint and confirm rehydration restores UX bits.
Verification matrix

Observed symptom → likely cause → first remediation

A compact mapping to accelerate fixes.

  • Full reload on back: likely sync unload or service worker-triggered reload → first try: remove synchronous unload work and re-test.
  • Partial state loss: ephemeral state not checkpointed → first try: capture minimal UI snapshot at suspend and reapply on restore.
  • Handlers re-running: event registration without guards → first try: make registrations idempotent or gate by a restored-flag.
Transition cues and UX

Signaling restores to users and graceful fallbacks

How to avoid surprising users when restore behavior differs from a reload.

Prefer subtle cues: brief non-blocking toasts such as "Restored your last view" or preserving scroll and focus so the user notices continuity. On a full reload, show a progressive state rehydration — skeletons that are resolved as persisted pieces arrive. Avoid modal interrupts during restore; instead surface a small, dismissible hint if critical resources need reconnecting.

Mini timeline recap

Diagnosis → fix → verify

Three prioritized actions to run on a failing page.

  1. Replicate with devtools open and observe console/network to classify: restore vs reload.
  2. Remove one suspected opt-out (sync unload / active socket) and retest to isolate cause.
  3. Apply a resumable init or minimal checkpoint, then verify across browsers and device types.
Classify
Console + network → restore or reload
Isolate
Disable suspect handlers or resources
Patch
Add resumable init and reverify with back/forward