ESC

Type to search the knowledge base.

visibilitychange and Page Hide

visibilitychange, pagehide, and pageshow: flush analytics, pause media, and survive mobile backgrounding.

intermediate3 min read
  • browser
  • visibilitychange
  • pagehide
  • page-lifecycle
  • analytics

Mobile users background your tab constantly. blur/focus are incomplete; unload is unreliable and bfcache-hostile. The durable pair is visibilitychange for “can the user see this?” and pagehide / pageshow for navigation lifecycle including bfcache.

Docs: Page Visibility, pagehide, Page Lifecycle.

visibilitychange

document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'hidden') {
    pauseRealtime();
    flushAnalytics();
  } else if (document.visibilityState === 'visible') {
    resumeRealtime();
  }
});

document.hidden / visibilityState reflect whether the page is visible. Use for:

  • Pausing video and WebGL
  • Throttling polling
  • Sending beacons while the page can still network

pagehide / pageshow

window.addEventListener('pagehide', (event) => {
  // event.persisted === true → page may enter bfcache
  navigator.sendBeacon('/exit', payload());
  if (!event.persisted) {
    closeExclusiveLocks();
  }
});

window.addEventListener('pageshow', (event) => {
  if (event.persisted) {
    // restored from bfcache — not a cold load
    revalidateSession();
  }
});
Event Fires when
pagehide Page is being unloaded or put into bfcache
pageshow Page shown, including bfcache restore
visibilitychange Visibility flips (tab switch, minimize)

sendBeacon and keepalive

navigator.sendBeacon('/ingest', blob);
// or
fetch('/ingest', { method: 'POST', body, keepalive: true, credentials: 'include' });

Prefer these on hidden/pagehide paths so the request outlives the page.

Don’t use unload

// Avoid
window.addEventListener('unload', () => { /* ... */ });

Breaks bfcache eligibility and often never runs on mobile process death. See bfcache.

SPA notes

Client-side route changes may not fire pagehide. Hook your router for soft navigations and still keep visibility handlers for tab switches.

Interview out-loud

“I flush analytics and pause media on visibilitychange hidden, use pagehide with sendBeacon instead of unload, and on pageshow persisted I revalidate because bfcache restored the page with old JS state.”

Double-firing guards

visibilitychange to hidden and pagehide can both fire on the same leave. Use a flushed flag so analytics doesn’t double-count sessions. Reset the flag on pageshow when appropriate.

Media and realtime matrix

Resource on hidden on visible / pageshow persisted
<video> pause optional resume
WebSocket close or ping backoff reconnect + resubscribe
setInterval poll stop restart
WebRTC mute/pause tracks renegotiate if needed
Draft form persist to IDB rehydrate

Write the matrix into the team wiki; different features forget different rows.

Further depth

Teams often under-invest in this topic until an incident or CWV regression. Schedule a one-hour drill: reproduce the failure mode in DevTools, list the top three mitigations for your stack, and file tickets with owners. Revisit after the next major feature that touches networking, rendering, auth, or third parties — those are the moments regressions land. Keep primary documentation links in the runbook so on-call is not searching chat history at 2am.

Concrete artifacts to leave behind: a short architecture note, a CI assertion or header snapshot, and a dashboard panel (lab or field) that would have caught the last bug. Teaching the rest of the team the mental model matters as much as the one-line fix.

Further reading

Related guides