一个仪表盘组件每两秒轮询一次接口,保持数字实时刷新。你切到Slack回消息,四分钟后还在那边,这个组件依然每两秒发一次请求——往一个你早就没在看的标签页里发。

把这个场景乘以用户打开的标签页数量,就不是四舍五入能忽略的误差了。这是服务器负载,是电池消耗,产出的价值是零。因为唯一能阻止它的信息——到底有没有人在看——从来没人检查过。

打开网易新闻 查看精彩图片

直觉方案有个坑

最本能的修法是:失焦时暂停,聚焦时恢复。代码读起来很干净:

监听blur事件清掉定时器,监听focus事件重新开始轮询。看起来没毛病,直到某天有人报了个奇怪的bug:一个本该在后台继续播放的视频,点开DevTools就暂停了。或者一个实时行情组件,打开浏览器扩展弹窗的瞬间就停止更新。还有一个在多显示器环境下特别容易漏掉的——仪表盘明明完整显示在第二块屏幕上,却进入了"空闲"状态,因为用户正在主屏的另一个应用里打字。

这些标签页没有一个是被隐藏的。内容就在屏幕上摆着。blur照样触发了,因为blur和focus回答的问题,比你真正想问的要窄得多。

你真正想问的问题

blur和focus告诉你的是窗口有没有键盘焦点。这对"输入框要不要显示聚焦光环"这类需求确实有用——但它不等于"人类当前能不能看到这个内容"。窗口完全可以在保持渲染和可见的情况下失去焦点,这正是上面第二块屏幕的场景。MDN的官方文档在解释为什么需要Page Visibility API时,专门拿这个例子来说明,而不是直接复用focus事件。

真正回答这个问题的API很小,就三样东西:

  • document.visibilityState——返回"visible"或"hidden"
  • document.hidden——同一个东西的旧版布尔简写
  • visibilitychange事件——状态翻转时在document上触发

标签页被切走、窗口最小化、或者(在多数平台上)屏幕锁定时,它才变成"hidden"——这些条件意味着内容在浏览器看来,肯定不在任何人的视网膜上。上面第二块屏幕的场景里它保持"visible",因为事实就是如此。这个API在各大常青浏览器里无前缀支持已经超过十年,连特性检测都不用做。

正确的接法

要换的是监听对象,不只是换个词:

把blur/focus的监听换成visibilitychange,在状态变成"hidden"时停掉轮询,变回"visible"时恢复。就这么简单。你的服务器少了一堆没人看的请求,用户的电池也省了。

下次再写轮询逻辑,先问一句:我要知道的是"窗口有没有焦点",还是"有没有人真的在看"?这两个问题的答案,经常不一样。