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.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Mastering the visibilitychange Event: Detect and React to Page Visibility Changes

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

visibilitychange

only 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

visibilitychange

only 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.

JavaScriptweb performancevisibilitychangePage Visibility APIbrowser events
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.