JavaScript Page Visibility Table

PieceWhat it doesField note
document.visibilityStateThe state read'visible' or 'hidden' - read explicitly; the boolean shorthand invites inverted logic
visibilitychange (document)The switch eventFires on tab/app switch, minimize, screen lock - one listener, two branches
pause on hiddenStop time-based workPolling, video, animation loops - the platform clamps them anyway; do it with intent
resync on visibleRecover freshRe-fetch once and reset timers - never trust accumulated ticks across a background gap
background throttlingThe platform rulesTimers clamp to 1/sec (chains to 1/min), rAF stops entirely - workers have separate budgets
prerender branchThe invisible loadSpeculation-prerendered pages run code before being seen - defer non-deferrable work to activation
hidden-time analyticsHonest dwellSum visible spans, not wall clock - background tabs inflate time-on-page by minutes
beacon on hiddenThe last reliable momentsendBeacon/keepalive on transition to hidden - beforeunload is unreliable on mobile and bfcache-hostile
Reference: the MDN Page Visibility API guide. The API answers one question - can the user see this tab - and its discipline is symmetrical: hide = pause with intent, visible = resync from the source of truth. The throttling table is the reason: background timers clamp, rAF stops - so unpaused work does not run correctly, it just burns battery while lying about it.
Bottom line: treat visibilityState as a strong hint, not a guarantee - app switchers and Picture-in-Picture can report visible while the user is gone, so anything money- or time-critical anchors to server clocks. The single highest-leverage fix most dashboards need: dwell time computed from visible spans instead of wall clock; background tabs are why engagement numbers never made sense.
Related tools: the requestAnimationFrame table (the loop that stops when hidden), the timers table (the clamps in detail), the Idle Detection table (user-idle versus tab-hidden, two different questions), and the Web Locks table (coordinating across those background tabs).

Page Visibility tells you whether the user can actually SEE your tab: document.visibilityState is 'visible' or 'hidden', and visibilitychange fires on every switch - tab change, app switch, minimize, screen lock. It is the primitive behind pause-on-hide video, stop-polling-when-hidden dashboards, and games that freeze instead of accumulating input while unattended.

Bottom line: the browser throttles background tabs anyway - timers clamp to once per second, rAF stops entirely - so your hidden-state code is not optional politeness, it is catching up with what the platform already does. The discipline: on hidden, pause everything time-based (timers, polling, video, animation loops); on visible, resync from the source of truth instead of trusting accumulated ticks.

The honest part: document.hidden lies in exactly one case that matters - on mobile, the app switcher can keep your page technically visible while the user is gone, and Picture-in-Picture or screen-off behaviors vary by platform. Treat visibilityState as a strong hint, not a guarantee, and anchor anything money- or time-critical to server timestamps instead of client-visible time.

How to use

  1. React to switches: document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'hidden') pauseAll(); else resumeAll(); }) - one listener, two branches, the entire pattern.
  2. Pause time-based work on hidden: clearInterval your polling, video.pause() your playback, stop your rAF loop - resuming them while hidden only gets you clamped timers and burned battery.
  3. Resync on visible instead of accumulating: on return, re-fetch the dashboard once and reset local timers - a 20-minute backgrounded tab that trusted setInterval ticks is 20 minutes of stale data wearing a live UI.
  4. Log honest sessions: track hidden timestamps for analytics - time-on-page that counts background tabs overstates engagement by minutes; visibility-aware timing is the real dwell time.
  5. Mind the exceptions: PiP windows and app switchers may keep visibilityState = 'visible' - for money-critical timing, anchor to server clocks and never bill by client-visible time alone.

Frequently asked questions

What is the difference between document.hidden and visibilityState?

visibilityState is the full state ('visible', 'hidden', and historically 'prerender'/'unloaded' before those phases moved to dedicated APIs); hidden is the boolean shorthand for hidden === 'visible' being false. Modern code should read visibilityState and compare explicitly - hidden reads as a negation that gets misspelled into inverted logic surprisingly often, and prerender (speculation-prerendered pages) deserves its own branch: your page may be loaded and RUNNING before the user ever sees it, which is why prerendered pages defer non-deferrable work until activation.

What exactly do browsers throttle in background tabs?

The clamp list grew over the years: timers (setInterval/setTimeout) run at most once per second after ~5 seconds hidden (intensive throttling can push that to once per minute for chained timers on inactive pages), requestAnimationFrame stops entirely, and some budgets hit network-priority and scheduler queues. The consequence: your background music scheduler, animated countdown, or 10-second poll all silently degrade - which is the point. Code that must run while hidden (alarm while the tab is backgrounded) needs a Web Worker (its own timer budget) or a Service Worker push, not a heroic setInterval.

Should I pause video and games when the tab hides?

Yes for games (freeze the loop, accumulate nothing - rAF stopping already freezes rendering, but your physics must not simulate 600 catch-up frames on return), and yes for video by convention: every major player pauses on hidden, and browsers may do it for you with media-session behaviors - doing it yourself keeps the UX consistent and your analytics honest. The subtle part is resume: re-fetch ad schedules or stream health, re-sync subtitles, and let the user resume where they were - pause state, not play state.

How does visibility relate to beforeunload and session analytics?

visibilitychange to 'hidden' is the modern de-facto LAST RELIABLE MOMENT to send session-end beacons (fetch keepalive or sendBeacon) - beforeunload and unload are unreliable on mobile and being deprecated for bfcache reasons. The honest dwell metric: sum of visible-time spans, not wall clock since load. A tab open in the background for two hours reports zero additional dwell under visibility-aware accounting - and that correction is usually the single biggest fix to engagement dashboards that never made sense.

Related tools