Saving Resources When the Tab Is Inactive
A tab in the background keeps running unchanged in most applications, polling intervals fire, animations run, videos play unheard. The Page Visibility API provides a reliable signal through document.hidden and the visibilitychange event, letting you throttle exactly this background work and noticeably save CPU, battery and network, without the user ever noticing.
Table of Contents
- 1. Why visibility is more than window focus
- 2. document.hidden and visibilitychange in detail
- 3. Throttling polling intervals instead of stopping them
- 4. Pausing animations and canvas rendering
- 5. Controlling video and audio playback
- 6. Measuring time on page correctly
- 7. Interaction with beforeunload and pagehide
- 8. Pitfalls: minimizing, screen lock and mobile browsers
- 9. Page Visibility API compared to related signals
- 10. Summary
- 11. FAQ
1. Why visibility is more than window focus
Many developers initially confuse the blur/focus event pair with what the Page Visibility API actually reports. A window can lose focus while remaining visible, for example when a user types in another application while the browser stays visible next to it. Conversely, a page can be invisible without losing focus, for example while sitting in a background tab even though the browser itself has focus. The Page Visibility API answers exactly the question that matters for resource conservation: is this document currently visible to the user or not.
Through the property document.hidden and the more detailed document.visibilityState, this information can be queried synchronously, and the visibilitychange event on document notifies on every change. The Page Visibility API fires whenever a user switches tabs, minimizes the window, switches to another application, or, on mobile devices, sends the app to the background.
The practical benefit lies in being able to throttle background work the user does not perceive anyway. A polling interval, a canvas animation or automatic video playback in the background burns CPU cycles and battery without delivering any visible value. The Page Visibility API makes exactly these situations detectable and controllable.
2. document.hidden and visibilitychange in detail
The simplest entry into the Page Visibility API is the combination of document.hidden as a boolean snapshot and the visibilitychange event for changes. In addition, document.visibilityState provides one of several possible string values, "visible", "hidden", or on some platforms also "prerender". In practice, the two states visible and hidden are entirely sufficient for most use cases.
An important detail with the Page Visibility API is checking the current state directly on page load, not just reacting to future changes. If a tab is already opened in the background, for example through a link that opens in a new background tab, no initial visibilitychange event fires, so the code must check the starting state explicitly.
// Track visibility state and react to changes
function watchVisibility(onVisible, onHidden) {
// Check the initial state immediately — no event fires for it
if (document.visibilityState === "hidden") {
onHidden();
} else {
onVisible();
}
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") {
onHidden();
} else {
onVisible();
}
});
}
watchVisibility(
() => console.log("Tab is visible — resume full activity"),
() => console.log("Tab is hidden — throttle background work")
);
3. Throttling polling intervals instead of stopping them
A classic use of the Page Visibility API is throttling polling intervals. An application that polls the server for new messages every five seconds does not need to keep the same cadence while the tab sits in the background. Instead of stopping the polling entirely, which would cause a noticeable delay on return, a significantly extended interval in the background is recommended, for example every 60 seconds instead of every 5.
This throttling through the Page Visibility API considerably reduces server load and battery usage without messages getting lost entirely. On returning to visibility, an immediate poll is also worthwhile, so the user sees current data right away when coming back to the tab, instead of waiting for the next regular interval.
// Adaptive polling: slow down in the background, refresh instantly on return
const VISIBLE_INTERVAL_MS = 5000;
const HIDDEN_INTERVAL_MS = 60000;
let pollTimer = null;
function schedulePoll() {
const delay = document.hidden ? HIDDEN_INTERVAL_MS : VISIBLE_INTERVAL_MS;
pollTimer = setTimeout(async () => {
await fetchLatestMessages();
schedulePoll();
}, delay);
}
document.addEventListener("visibilitychange", () => {
if (!document.hidden) {
// Tab became visible again — poll immediately, then resume schedule
clearTimeout(pollTimer);
fetchLatestMessages().then(schedulePoll);
}
});
schedulePoll();
4. Pausing animations and canvas rendering
Canvas animations, WebGL scenes and JavaScript driven CSS animations often run through requestAnimationFrame loops that modern browsers already throttle automatically to a very low rate once a tab is hidden, but do not fully stop. The Page Visibility API allows explicit control: instead of relying on the browser's implicit throttling, the application actively pauses the loop, saving even the last remaining cycles.
For complex WebGL scenes with many particles or physics calculations, this difference matters measurably, especially on devices with limited battery capacity. The Page Visibility API combined with a clean start/stop of the requestAnimationFrame loop prevents unnecessary GPU work that no user will ever see.
5. Controlling video and audio playback
For video players, the Page Visibility API is a standard tool for pausing automatic playback once a tab moves to the background and resuming it on return. Unlike pure background animations, caution is needed here: a user often wants audio or podcasts to keep playing in the background while working in another tab. Blanket pausing on every visibilitychange would be counterproductive in that case.
The correct solution distinguishes between pure video content, which usually makes no sense without a visible screen, and audio only content that should explicitly keep playing. The Page Visibility API only provides the signal, the decision of what should happen on invisibility remains part of the product logic and should be configurable.
// Pause video (but not audio-only content) when the tab is hidden
const videoElement = document.querySelector("video");
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
if (!videoElement.dataset.audioOnly) {
videoElement.pause();
}
} else if (videoElement.dataset.wasPlaying === "true") {
videoElement.play();
}
});
videoElement.addEventListener("pause", () => {
videoElement.dataset.wasPlaying = document.hidden ? "true" : "false";
});
6. Measuring time on page correctly
Analytics implementations that want to measure "time on page" absolutely must account for the Page Visibility API, otherwise a tab forgotten in the background counts for hours even though no one is actually looking at the page. The correct approach only sums the time intervals during which document.visibilityState is "visible", and pauses the counter on every switch to "hidden".
This measurement method through the Page Visibility API delivers considerably more reliable metrics for engagement analysis than a simple difference between page load and page exit. Many commercial analytics solutions such as Google Analytics already use similar mechanisms internally, custom implementations should apply the same principle consistently.
// Track accumulated visible time, ignoring hidden intervals
let visibleSince = document.hidden ? null : Date.now();
let totalVisibleMs = 0;
document.addEventListener("visibilitychange", () => {
const now = Date.now();
if (document.hidden && visibleSince !== null) {
totalVisibleMs += now - visibleSince;
visibleSince = null;
} else if (!document.hidden) {
visibleSince = now;
}
});
function getEngagementSeconds() {
const current = visibleSince ? Date.now() - visibleSince : 0;
return Math.round((totalVisibleMs + current) / 1000);
}
7. Interaction with beforeunload and pagehide
The Page Visibility API does not replace the events around leaving a page, but complements them meaningfully. On mobile devices, an app is often terminated without any unload event at all, the operating system simply kills the process once memory is needed. The only reliable last checkpoint in this case is the switch to document.visibilityState === "hidden", which is why critical save operations, such as saving a form draft, should be tied to this event rather than to beforeunload or unload.
For sending analytics beacons on exit, the combination of visibilitychange with state hidden and the sendBeacon() method is the recommended pattern, because beforeunload fires unreliably or not at all on many mobile platforms. The Page Visibility API is therefore more robust than the classic unload events and should be preferred in modern implementations.
8. Pitfalls: minimizing, screen lock and mobile browsers
A common mistake when using the Page Visibility API is assuming that "hidden" is equivalent to "the user is gone". On macOS, a window is often still reported as visible even when it is fully covered by other windows, because the operating system technically interprets "visible" as "rendered", not "within the field of view". For true attention measurement you therefore additionally need Intersection Observer or similar signals.
On mobile devices, locking the screen reliably triggers a visibilitychange to hidden, but the exact moment the operating system actually pauses or kills the tab can vary. iOS Safari treats background tabs considerably more aggressively than desktop Chrome and may fully reload a page once the user returns if memory pressure was high. Code built on the Page Visibility API should therefore never assume that state is guaranteed to survive between visibility changes.
9. Page Visibility API compared to related signals
Besides the Page Visibility API, there are several related browser signals that are easily confused. Choosing the right signal depends on the exact use case.
| Signal | Measures | Triggered by | Typical use |
|---|---|---|---|
| Page Visibility API | Tab visible or hidden | Tab switch, minimizing, app in background | Throttle polling, pause video |
| focus/blur | Window has keyboard focus | Click into another app, Alt-Tab | Input fields, keyboard shortcuts |
| Intersection Observer | Element within the visible viewport | Scrolling, layout change | Lazy loading, per element visibility tracking |
| pagehide/pageshow | Page left or restored from BFCache | Navigation, back button | State restoration after BFCache |
In practice, these signals complement each other. The Page Visibility API is the right tool for anything related to overall tab visibility, while Intersection Observer is responsible for individual elements within a visible page. Together they give a complete picture of what the user is actually perceiving.
Mironsoft
JavaScript development, browser APIs and modern web applications
Want to cut battery and CPU load in the background?
We analyze your application for unnecessary background work and integrate the Page Visibility API for throttled polling, paused animations and correct engagement measurement.
Performance audit
Identify unnecessary background work and throttle it with document.hidden
Media control
Pause video and animation without unwanted stopping of audio
Analytics correction
Measure time on page correctly through visible time, not raw session length
10. Summary
The Page Visibility API is a small but powerful tool for making web applications more resource friendly. With document.hidden, document.visibilityState and the visibilitychange event, it can be reliably detected whether a tab is currently visible to the user, and based on that, polling can be throttled, animations paused and video playback controlled.
Anyone using the Page Visibility API in production should check the initial state explicitly on load, differentiate between video and audio content, tie critical save operations to hidden rather than unload, and stay aware that "visible" does not necessarily mean "within the user's field of view". Combined with Intersection Observer for element level visibility, this creates a complete picture of actual user attention.
Using the Page Visibility API Effectively — Key Takeaways
Core signal
document.hidden and visibilitychange reliably show whether a tab is currently visible, independent of window focus.
Polling and animation
Extend intervals in the background instead of stopping them entirely, actively pause requestAnimationFrame loops.
Analytics
Sum time on page only during visible intervals, not while the tab sits in the background.
Robustness
Tie critical save operations to hidden instead of unload, especially on mobile devices.