Mastering the visibilitychange Event: Detect and React to Page Visibility Changes
This article explains the native visibilitychange event of the Page Visibility API, its core properties document.hidden and document.visibilityState, provides complete JavaScript examples for listening and removing the event, discusses practical use cases such as media control, request throttling, form saving, outlines browser compatibility, and highlights limitations and best‑practice considerations.
What is the visibilitychange event?
The visibilitychange event is a native browser event and a core part of the Page Visibility API. It fires whenever the page’s visibility state changes (e.g., from "visible" to "hidden" or vice‑versa). In environments like uniapp or WeChat Mini‑Programs, similar APIs such as apphide and appshow are provided.
Core purpose : Determine whether the page is in a user‑visible (foreground) or invisible (background) state, such as when switching tabs, minimizing the browser, or locking the phone.
Key Properties and Events
1. document.hidden (state‑checking core)
Boolean: true means the page is currently invisible (background); false means the page is visible (foreground).
Directly reflects the page’s visibility status.
2. document.visibilityState (detailed state description)
String returning one of four possible values: visible: page is at least partially visible (e.g., the tab is active and the window is not minimized). hidden: page is not visible (e.g., another tab is active, the window is minimized, or the app is in the background). prerender: page is being prerendered (user has not seen it yet; supported by some browsers). unloaded: page is about to be unloaded (e.g., the tab is being closed; support varies).
In practice, hidden and visible are the most commonly used states.
3. visibilitychange event
Triggered when document.visibilityState or document.hidden changes. Listen with document.addEventListener.
Basic Usage (Full Code Example)
1. Listening Logic
// Listen for visibilitychange event
document.addEventListener('visibilitychange', handleVisibilityChange);
// Event handler
function handleVisibilityChange() {
// Method 1: use document.hidden
if (document.hidden) {
console.log('Page entered background (invisible)');
// Background logic: pause video, clear timers, save data, etc.
pauseVideo();
clearInterval(timer);
saveUserState();
} else {
console.log('Page returned to foreground (visible)');
// Foreground logic: resume video, restart timers, refresh data, etc.
playVideo();
restartTimer();
refreshData();
}
// Method 2: use document.visibilityState for finer granularity
switch (document.visibilityState) {
case 'visible':
console.log('Page visible (foreground)');
break;
case 'hidden':
console.log('Page hidden (background)');
break;
case 'prerender':
console.log('Page prerendering');
break;
case 'unloaded':
console.log('Page about to unload');
break;
}
}2. Removing the Listener (Avoid Memory Leaks)
// Remove listener before page unload
window.addEventListener('beforeunload', function() {
document.removeEventListener('visibilitychange', handleVisibilityChange);
});
// Or in a Vue/React component's destroy hook (Vue example)
export default {
beforeDestroy() {
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
}
};Typical Scenarios
1. Media Playback Control
const video = document.getElementById('myVideo');
function handleVisibilityChange() {
if (document.hidden) {
video.pause(); // Pause when page goes to background
} else {
video.play(); // Resume when page returns to foreground
}
}2. Reducing Unnecessary Requests
let pollTimer;
function startPoll() {
pollTimer = setInterval(() => {
fetch('/api/refresh'); // Periodic polling
}, 5000);
}
function stopPoll() {
clearInterval(pollTimer);
}
function handleVisibilityChange() {
if (document.hidden) {
stopPoll(); // Stop polling in background
} else {
startPoll(); // Restart polling in foreground
}
}3. Recording User Online Status
let lastLeaveTime;
function handleVisibilityChange() {
if (document.hidden) {
lastLeaveTime = new Date().getTime(); // Record time of background entry
reportUserState('offline'); // Report offline status
} else {
const onlineTime = new Date().getTime() - lastLeaveTime;
console.log(`User offline duration: ${onlineTime}ms`);
reportUserState('online'); // Report online status
}
}4. Preventing Form Data Loss
function saveFormDraft() {
const formData = {
username: document.getElementById('username').value,
content: document.getElementById('content').value
};
localStorage.setItem('formDraft', JSON.stringify(formData));
}
function handleVisibilityChange() {
if (document.hidden) {
saveFormDraft(); // Save draft when page goes to background
}
}Compatibility and Browser Support
Desktop : Chrome, Firefox, Edge, Safari (≥6.1) fully support.
Mobile : WeChat built‑in browser, Chrome for Android, Safari iOS (≥7.1), Android system browsers (≥4.4) fully support.
Legacy : IE10+ supports the event with vendor prefixes msVisibilityChange and msHidden (generally negligible market share).
Important Considerations
1. Difference from pagehide / pageshow
visibilitychangeonly concerns page visibility (foreground/background) and does not indicate whether the page is being unloaded. pagehide fires when the page is about to be unloaded (e.g., tab close) and may also fire on backgrounding, making it less precise for visibility checks.
Conclusion: Use visibilitychange to detect background/foreground switches; use pagehide to detect actual page unloads.
2. Special Cases in WeChat / Alipay In‑App Browsers
These browsers fully support visibilitychange without extra SDKs.
When sharing to a WeChat friend, the page briefly goes hidden then visible again, which may trigger a false background event. Business logic should account for this.
3. Avoid Overuse
Frequent heavy DOM operations inside visibilitychange can affect performance. Limit the handler to essential actions such as pausing/resuming media or saving state.
4. Lock‑Screen Triggers
On mobile lock‑screen, the page becomes hidden ( document.hidden = true) and resumes visibility after unlocking.
5. Inability to Distinguish Exact Hide Reasons
visibilitychangeonly reports "visible" or "hidden"; it cannot tell whether the user switched to another app, another browser tab, locked the screen, or performed a temporary share action.
6. Inconsistent Timing in Some Browsers
In WeChat/Alipay and on iOS Safari, the event may be delayed or missed when the page is frozen due to low memory, affecting timers and background logic.
7. Cannot Reliably Detect Full‑Screen Visibility
document.visibilityState = 'visible'only guarantees the page is at least partially visible. For full‑screen checks, combine with document.fullscreenElement or similar APIs.
Advanced Distinguishing Techniques
1. Distinguish Tab Switch vs. App Switch
let isTabSwitch = false;
let timer;
// Listen for visibility changes
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
// Start a short timer; a noticeable delay suggests an app switch (browser frozen)
timer = setTimeout(() => {
console.log('Possible switch to another App');
}, 100);
} else {
clearTimeout(timer);
if (isTabSwitch) {
console.log('Possible switch to another browser tab');
isTabSwitch = false;
}
}
});
// Listen for focus loss
window.addEventListener('blur', () => {
if (document.hidden) {
isTabSwitch = true; // Lost focus while hidden → likely a tab switch
}
});2. Distinguish Phone Lock vs. Window Minimize
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
if (typeof screen.brightness !== 'undefined' && screen.brightness < 0.1) {
console.log('Likely phone lock');
} else {
console.log('Likely window minimized');
}
}
});3. Distinguish Share‑Then‑Return vs. Page Close (WeChat Example)
let isSharing = false;
// Assume a share button with id "shareBtn"
document.getElementById('shareBtn').addEventListener('click', () => {
isSharing = true;
});
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
if (isSharing) {
console.log('Possible temporary leave after WeChat share');
setTimeout(() => {
if (document.hidden) {
console.log('Share completed without return – page may be closed');
isSharing = false;
}
}, 30000);
} else {
console.log('Possible other reason for hide/close');
}
} else {
if (isSharing) {
console.log('Returned after sharing');
isSharing = false;
}
}
});4. Detect Actual Page Close
let isPageClosed = false;
window.addEventListener('pagehide', () => {
isPageClosed = true;
console.log('Page has been closed');
});
document.addEventListener('visibilitychange', () => {
if (document.hidden && !isPageClosed) {
console.log('Page hidden but not closed – possible temporary leave');
}
});Conclusion
There is no perfect solution; the visibilitychange event is the most reliable way to detect foreground/background switches, but developers should combine it with other signals (focus/blur, timers, platform‑specific APIs) to improve accuracy for complex scenarios.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
