Skip to content
Foxipa

Suggest an edit

Tell us what should be corrected, clarified, or improved. Submission will be connected to Foxipa’s contribution service later.

No information is sent anywhere yet. Your suggestion is only prepared in your browser.

Learning

5.2 The lifecycle flusher: recipe, bfcache, and why unload is out

One flush per hidden transition, bfcache restores identified precisely via persisted pageshow, and the layered case against unload.

Levels: Intermediate ~16 min read

Intermediate

The lifecycle rules from the previous page compile into one small class: flush exactly once per hidden transition, notice back/forward-cache restores without hallucinating them, and never touch unload. This page builds that class, then defends each design decision against the failure modes that real analytics SDKs have shipped with.

The recipe

The flusher wires three listeners around one latch. The visibilitychange handler checks the state and flips the latch before flushing — so a re-entrant event or the pagehide that follows cannot double-send. The pagehide listener is a fallback for the rare ordering where it arrives without a prior visibility change; the latch makes it a no-op in the normal sequence. And pageshow with persisted === true is the only restore signal — for reasons the next section makes precise:

Interactive Runnable example
Edit

Ready to run.

The output is the contract in miniature: one flush for the tab-switch, no second flush when pagehide follows the hidden transition, nothing on plain tab-switch-back, and the restore hook firing exactly once for the persisted pageshow. The injectable getVisibility and send options exist so tests can drive the state machine — in production the defaults read the real document and call the real navigator.sendBeacon.

bfcache: why restores must be identified precisely

A page restored from the back/forward cache resumes mid-execution: timers, listeners, and module state survived the freeze. Two consequences follow. Analytics must count the restore as a new pageview — tagged as a restore, or dashboards double-count nothing but undercount returns — and anything session-scoped may need re-initializing. The trap is that visibilitychange → visible fires on every ordinary tab-switch-back too; only pageshow with persisted distinguishes an actual bfcache restore from a user coming back from lunch. The flusher therefore unlatches its state on the visible transition (so the next hidden can flush) but defers the restore callback to the persisted pageshow — splitting the two concerns that naive SDKs merge into one spurious 'restore' per tab switch.

Why unload and beforeunload are absent

The recipe registers no unload or beforeunload listeners, and the reasoning is layered. Reliability: neither fires in the scenarios where mobile pages actually die — app switches and OS kills — so code keyed on them is silent exactly when it matters most. Compatibility cost: pages that register unload handlers are excluded from bfcache in major engines, trading away the fastest possible back-navigation for a signal that arrives unreliably anyway; browsers have been progressively deprecating the event on those grounds (a browser-policy evolution — not spec text — that current engine documentation tracks). Redundancy: everything unload could still deliver here, the hidden transition already delivers, earlier and with a spec guarantee behind it. When a migration genuinely requires touching the old events, the flusher's flushNow() is the escape hatch — callable from anywhere, including a guard you control, without the lifecycle core ever depending on those events.

Quick check

The page fires visibilitychange → visible after a plain tab switch back. What should the flusher do?

// inside the visibilitychange handler
if (document.visibilityState === 'visible') {
  
}

Key takeaways

  • One flush per hidden transition: a latch set before flush() makes visibilitychange-then-pagehide idempotent.
  • pagehide stays as a latched fallback; flushNow() is the manual escape hatch.
  • bfcache restore = pageshow with persisted === true — never the visible transition alone.
  • No unload/beforeunload: unreliable on mobile, bfcache-hostile, redundant given the hidden-transition guarantee.
  • Injectable getVisibility/send make the whole state machine testable without a browser's cooperation.
Was this page helpful?

Mark this learning resource complete to track your learning progress.