The Page Visibility API tells your JavaScript code whether the current document is visible to the user or running in the background.
The API is intentionally small. It adds one event, visibilitychange, and two properties on document: document.hidden and document.visibilityState.
It is supported across modern browsers:
https://caniuse.com/pagevisibility
This API is useful whenever work should slow down or pause while the page is not visible. Typical examples are pausing media playback, reducing polling frequency, postponing non-essential UI updates, or delaying analytics work until the user returns to the tab.
Browsers already throttle some background work, such as timers and requestAnimationFrame() callbacks, but handling visibility explicitly still gives your application more control over network traffic, CPU usage, and user experience.
The following example listens for visibilitychange, updates the page title, and sends a request to a Spring Boot backend so you can see exactly when the event fires.
const serverUrl = 'http://localhost:8080';
document.addEventListener('visibilitychange', handleVisibilityChange);
async function handleVisibilityChange() {
const state = document.visibilityState;
document.title = `Visibility: ${state}`;
const endpoint = document.hidden ? 'hidden' : 'visible';
await notifyServer(endpoint);
}
async function notifyServer(endpoint) {
try {
await fetch(`${serverUrl}/${endpoint}`, {
method: 'GET',
keepalive: true
});
} catch (error) {
console.error('Failed to notify server', error);
}
}
void handleVisibilityChange();
If you open the application in two tabs, the active tab reports visible, while the inactive tab reports hidden.

On desktop browsers, the event fires when you switch tabs or minimize the window. On mobile devices, it typically fires when the browser moves to the background and when the screen is locked or unlocked while the page is open.
In modern browsers, document.visibilityState is typically either visible or hidden. In most cases, document.hidden is the most convenient way to branch between foreground and background behavior.
Reusable helpers ¶
For larger applications, it is often convenient to extract common visibility patterns into a small helper module built directly on top of the native API.
The sample project contains a runWhenVisible() helper. It runs the callback immediately if the page is already visible. Otherwise, it waits for the next time the document becomes visible and runs the callback once.
function runWhenVisible(callback) {
if (!document.hidden) {
callback(document.visibilityState);
return () => {};
}
const handleChange = () => {
if (document.hidden) {
return;
}
document.removeEventListener('visibilitychange', handleChange);
callback(document.visibilityState);
};
document.addEventListener('visibilitychange', handleChange);
return () => document.removeEventListener('visibilitychange', handleChange);
}
The createVisibilityInterval() helper schedules work with one interval while the page is visible and, optionally, a slower interval while it is hidden. In the demo, one timer writes to the console every second and stops after five seconds, while another polls the backend every 15 seconds in the foreground and every minute in the background.
const stopConsoleTimer = createVisibilityInterval({
visibleInterval: 1000,
callback: state => console.log(`tick while ${state}`)
});
window.setTimeout(stopConsoleTimer, 5000);
createVisibilityInterval({
visibleInterval: 15 * 1000,
hiddenInterval: 60 * 1000,
callback: async state => {
console.log(`poll while ${state}`);
await fetch(`${serverUrl}/poll`);
}
});
function createVisibilityInterval({
visibleInterval,
hiddenInterval = visibleInterval,
callback
}) {
let timeoutId = 0;
let stopped = false;
const scheduleNext = () => {
if (stopped) {
return;
}
const delay = document.hidden ? hiddenInterval : visibleInterval;
timeoutId = window.setTimeout(() => {
void runCallback();
}, delay);
};
const runCallback = async () => {
if (stopped) {
return;
}
await callback(document.visibilityState);
scheduleNext();
};
const handleChange = () => {
window.clearTimeout(timeoutId);
scheduleNext();
};
scheduleNext();
document.addEventListener('visibilitychange', handleChange);
return () => {
stopped = true;
window.clearTimeout(timeoutId);
document.removeEventListener('visibilitychange', handleChange);
};
}
The third helper, subscribeToVisibilityChange(), centralizes event registration and returns a cleanup function.
subscribeToVisibilityChange(state => {
console.log('visibilitychange event', state);
updateTitle(state);
});
function subscribeToVisibilityChange(callback) {
const handleChange = () => callback(document.visibilityState);
document.addEventListener('visibilitychange', handleChange);
return () => document.removeEventListener('visibilitychange', handleChange);
}
This approach keeps the code close to the platform, avoids extra dependencies, and still makes recurring visibility patterns easy to reuse.
This concludes our tour of the Page Visibility API. It is a small browser feature, but it is very effective when you want your application to be more efficient and behave more naturally in background tabs.
You can find the source code for all presented examples on GitHub:
https://github.com/ralscha/blog2019/tree/master/visibility