Back

如何检测浏览器标签页变为非活动状态

如何检测浏览器标签页变为非活动状态

你有一个每十秒触发一次的轮询定时器、一个在后台播放的视频,或者一个不停运行的动画循环——它们都在为五分钟前就已切换到其他标签页的用户消耗资源。一旦你了解了应该调用哪个 API,解决这个问题就轻而易举。

核心要点

  • Page Visibility API 是检测标签页可见性变化的现代且可靠的方式,取代了旧有的 window.onfocuswindow.onblur 方案。
  • 使用 document.hiddendocument.visibilityState 配合 visibilitychange 事件,可以在标签页变为非活动或活动状态时作出响应。
  • 实际应用场景包括暂停轮询、停止媒体播放、自动保存表单草稿,以及发送会话结束分析数据。
  • visibilitychangepagehidenavigator.sendBeacon() 结合使用可实现可靠的会话结束追踪,并应避免使用 unload 事件,因为它会破坏前进/后退缓存。
  • 在单页应用中销毁组件时务必移除监听器,以防止内存泄漏。

正确的工具:Page Visibility API

旧的解决方案依赖于 window 的 focus 和 blur 事件——即 window.onfocuswindow.onblur。它们检测的是浏览器窗口是否拥有键盘焦点,而不是标签页是否真的可见。在分屏模式或多窗口工作流中,一个标签页可能完全可见但没有焦点,反之亦然。它们在不同浏览器中对标签页切换的处理也表现得不一致。

Page Visibility API 妥善地解决了这个问题。它能准确告诉你页面是否对用户可见,与焦点状态无关。

两个属性可以告诉你当前状态:

  • document.visibilityState —— 返回 "visible""hidden"
  • document.hidden —— 标签页隐藏时返回 true,可见时返回 false

现代浏览器不再需要厂商前缀。该 API 已被广泛支持,覆盖 Chrome、Firefox、Safari 和 Edge 的桌面端及移动端。

监听 visibilitychange 事件

visibilitychange 事件 会在标签页变为隐藏或重新可见时在 document 上触发。以下是基本模式:

document.addEventListener("visibilitychange", () => {
  if (document.hidden) {
    console.log("Tab is hidden — pause or reduce work");
  } else {
    console.log("Tab is visible — resume work");
  }
});

如果你需要字符串值而非布尔值,也可以直接读取 document.visibilityState

实际应用场景

暂停和恢复轮询

与其在用户切换到其他标签页时仍发起 API 请求,不如暂停定时器,并在用户返回时恢复它:

let intervalId = null;

function startPolling() {
  if (intervalId !== null) return;
  intervalId = setInterval(fetchData, 10000);
}

function stopPolling() {
  clearInterval(intervalId);
  intervalId = null;
}

document.addEventListener("visibilitychange", () => {
  document.hidden ? stopPolling() : startPolling();
});

startPolling();

注意:浏览器会节流后台标签页中的 setInterval,而不是完全停止它,因此在隐藏标签页中依赖时间精度本身就是不可靠的。显式地暂停会更清晰。防止重复创建定时器还能避免在 startPolling 被调用两次时多个定时器叠加。

暂停媒体播放

const video = document.querySelector("video");

document.addEventListener("visibilitychange", () => {
  if (document.hidden) {
    video.pause();
  } else {
    video.play().catch(() => {
      // Autoplay may be blocked by the browser; handle silently
    });
  }
});

.catch() 处理程序非常重要,因为 video.play() 返回一个 promise,当浏览器阻止自动播放时它可能会被拒绝,否则会抛出未捕获的错误。

自动保存表单草稿

在用户离开标签页时触发保存,而不是依赖固定的定时器:

document.addEventListener("visibilitychange", () => {
  if (document.hidden) {
    saveDraft();
  }
});

在用户离开前发送分析数据

对于会话结束数据,可以将 visibilitychange 事件pagehide 结合使用,并通过 navigator.sendBeacon() 在不阻塞导航的情况下可靠地发送数据:

document.addEventListener("visibilitychange", () => {
  if (document.visibilityState === "hidden") {
    navigator.sendBeacon(
      "/analytics",
      JSON.stringify({ event: "tab_hidden" })
    );
  }
});

window.addEventListener("pagehide", () => {
  navigator.sendBeacon(
    "/analytics",
    JSON.stringify({ event: "page_unload" })
  );
});

避免在这种场景下使用 unload 事件——在现代浏览器中它不可靠,且会破坏前进/后退缓存(bfcache),从而降低导航性能。在移动浏览器上,状态为 "hidden"visibilitychange 通常是判定会话结束的唯一可靠信号,因为 pagehidebeforeunload 并不总是会触发。

清理事件监听器

当监听器不再需要时一定要移除它们,特别是在单页应用中:

function handleVisibility() {
  console.log(document.visibilityState);
}

document.addEventListener("visibilitychange", handleVisibility);

// Later, when tearing down:
document.removeEventListener("visibilitychange", handleVisibility);

请记住,传递给 removeEventListener 的引用必须与 addEventListener 中使用的函数引用完全相同。内联的箭头函数无法被移除,因为每次都会创建一个新的引用。

结语

Page Visibility API——具体来说是 document.hiddendocument.visibilityState 以及 visibilitychange 事件——是检测浏览器标签页变为非活动状态的正确且现代的方法。它在所有当前浏览器中都能可靠工作,在 window 焦点和失焦事件处理不到位的标签页切换场景中表现准确,并能与 navigator.sendBeacon()pagehide 事件良好配合,处理会话结束逻辑。用它来停止不必要的工作、保存状态或追踪用户参与度,并在用完后清理你的监听器。

常见问题

两者都报告页面是否可见,但返回类型不同。document.hidden 返回布尔值,便于进行快速的条件判断。document.visibilityState 返回字符串,目前为 visible 或 hidden,描述性更强,并且在将来添加新状态时更具前瞻性。根据你的代码风格选择合适的即可。

浏览器对后台定时器进行节流而不是停止它们,通常将其限制为每秒一次或更慢。定时器仍在运行,但频率不可预测,因此对时间敏感的逻辑变得不可靠。最干净的解决方案是在 visibilitychange 处理程序中调用 clearInterval,并在标签页重新变为可见时重启定时器。

推荐使用 visibilitychange 结合 pagehide 和 navigator.sendBeacon。beforeunload 和 unload 事件在移动端不可靠,并且会破坏前进/后退缓存,损害性能。状态为 hidden 的 visibilitychange 事件是在桌面和移动浏览器上检测用户离开页面最一致的信号。

可以,但其行为取决于父级。iframe 通常继承其父文档的可见性状态。通过 CSS(如 `display: none`)隐藏 iframe 本身不会在 iframe 内部触发 `visibilitychange` 事件。每个 iframe 都有自己的 document 对象,因此你应该在 iframe 的 document 上监听 visibilitychange,而不是在顶层 window 上。

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay