我最早被 Page Visibility API 逼着去仔细看文档,是因为一个特别尴尬的线上事故。当时我在做一个给运营看的大屏页面,页面上有一组实时告警列表,前端每 15 秒拉一次后端接口。本来功能一切正常,直到某天产品跑过来说:后台打开这个标签页放着不管,再回来后,页面积压了几百条假告警,接口一恢复就像洪水一样往外喷。我一开始猜测是定时器被浏览器节流了,后来实测发现根本不是节流那么简单,而是页面在后台时,请求虽然发出去了,界面却完全没机会渲染,等回到前台又重新补跑了一堆不必要的任务。那次之后我把跟页面可见性相关的机制系统过了一遍,才发现 Page Visibility API 和 visibilitychange 事件虽然看着简单,但实际用起来,从事件的触发边界到不同浏览器的兼容细节,处处都有坑。
这篇文章就把我完整的理解写出来,适合正在做视频播放器、直播、数据大屏、消息通知页、H5 埋点上报这类项目的同学。凡是页面切走之后还想控制脚本是否继续工作的,都绕不开这几个 API。
1. 先说一个教训:为什么我只监听 blur 事件会翻车
1.1 我最初写的那版“土办法”
第一次修复线上问题时,我下意识想到的是监听 window.blur。因为直觉告诉我,用户把页面切走了,浏览器窗口应该失焦了,那我就暂停告警弹出提示,这个逻辑不是顺理成章吗?
当时的代码大概是这样的:
javascript复制window.addEventListener('blur', () => {
// 停止告警音频
// 停止轮询
stopAlarmPolling();
});
window.addEventListener('focus', () => {
// 恢复告警音频
// 重新拉取数据
startAlarmPolling();
});
从表面行为看,这确实能覆盖很多常见操作。比如用户在 Windows 上按 Alt + Tab 切换窗口,或者在 macOS 上 Cmd + Tab 切换应用,blur 基本都会触发。但真正线上出问题的场景是另一种:用户只是在浏览器里点了另一个标签页,比如从大屏标签页切到同事发的 IM 网页版,这时候当前页面所在的窗口仍然是聚焦的,blur 压根不会触发。
于是大屏页面认为自己还活在前台,定时器不断发起请求、告警音效不断尝试播放,把整条业务链路拖得很累,用户几乎无法第一时间收到真正重要的告警。
1.2 “页面不可见”和“窗口失焦”根本不是一回事
这个例子特别典型地反映了浏览器里好几组“看似能替代”的概念之间的差别:
window.blur表达的是“这个窗口不在焦点上”,不代表整个页面被另一个标签页遮住了。document.hidden表达的是“这个页面目前是否被用户实际看到”,也就是标签页是否真的被切换或者最小化了。- 更准确的状态描述是
document.visibilityState,它的值是'visible'还是'hidden',决定了你页面逻辑该不该继续跑前台任务。
如果只看 blur,你可能把“用户切换到别的应用窗口,但页面仍占据着当前标签页且被完整渲染”的情况误判为隐藏,导致音频被错误停掉,或者动画被错误暂停。相反,如果只看 visibilityState,那你可能也会漏掉一些用户注意力已经转移、但浏览器仍认为页面可见的细节。
所以,理解了这套可见性语义之后,再去看官方文档里的一句话就非常清楚了:Page Visibility API 是浏览器提供给页面的一个公共信号,用来告诉你“这个页面有没有被用户真实看到”。它不是简单的焦点问题,而是由标签页切换、窗口最小化、系统锁屏等多种因素共同决定的综合状态。
1.3 为什么轮询任务应该在 hidden 时不只停止,还要“冻结时间”
除了判断逻辑要对,另一个容易出错的是恢复逻辑。
用户切回页面时,你需要判断“离开期间漏掉了哪些消息”,而不是立刻把积压的请求全部打出去。我后来在代码里加了一个 lastVisibleTimestamp,用 visibilitychange 记录进入 hidden 的时间点,回到 visible 时先只请求增量数据,避免了回来时接口被瞬间刷爆的情况。这一点可能才是实际项目里真正要命的坑:事件本身不复杂,但事件引发的业务状态恢复策略,需要提前设计。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. visibilityState、hidden、visibilitychange:先搞清三个基础概念的分工
2.1 visibilityState 是标准的“状态结论”
现代浏览器里,建议你优先读 document.visibilityState,它直接给出一个枚举值,让人一眼就知道页面到底处于什么状态。根据规范,这个枚举值分为四种:
| 状态 | 含义 | 常见触发场景 |
|---|---|---|
visible |
页面至少部分可见 | 当前标签页处于前台且窗口未被最小化 |
hidden |
页面完全不可见 | 切换到其他标签页、最小化窗口、系统锁屏、移动端切到其他应用 |
prerender |
页面正在预渲染,但还没展示给用户 | 浏览器预加载某些页面链接时可能出现 |
unloaded |
文档正在被卸载 | 兼容性较差,实际使用极少,不建议依赖 |
prerender 这个状态很有迷惑性。在 Chrome 上如果你用预渲染机制,页面可能在用户点开之前就已经加载并执行了一段脚本,此时 document.visibilityState === 'prerender'。如果你在脚本里一进来就疯狂上报、播放声音、拉数据,预渲染阶段的行为就会变得不可控。正确姿势是遇到 prerender 时尽量保持“静默”,等它真正变成 visible 后再开始工作。
2.2 hidden 是“历史遗留但依然好用”的布尔值
document.hidden 其实是一个更老的 API。在老项目中,很多程序员会写:
javascript复制if (document.hidden) {
/
