打开网页计时器,倒数 25 分钟。切到另一个标签页处理点事,回来一看——倒数才走了 16 分钟,“少跑了”9 分钟。或者反过来:合上电脑去吃饭,回来打开页面发现倒数早结束了,但你没听到铃响。这些都不是 bug,是浏览器和操作系统对网页能做什么有非常明确的限制。这篇讲清楚网页计时器的真实精度边界。
标签页失焦:后台节流是常态
所有现代浏览器都会节流(throttle)非活动标签页的 JS 定时器。规则大同小异:
- Chrome / Edge:后台标签页
setTimeout/setInterval最小间隔 = 1 秒。连续静默 5 分钟以上进入”intensive throttling”,间隔进一步拉到 1 分钟。 - Firefox:默认 1 秒最小间隔,2017 年后类似 Chrome 的策略。
- Safari:策略更激进,后台标签页 setTimeout 可能完全冻结,唤醒时机不确定。
也就是说,你写 setInterval(updateUI, 100) 期待每 100 ms 更新一次,标签页一旦切到后台,最快也是 1 秒一次。如果你的”剩余时间”是靠 setInterval 每次 -1 累加的——切走 10 分钟 = 少了 600 次 tick = UI 显示”还剩很多”。
写法对比
错误:累加 tick
let secondsLeft = 25 * 60;
setInterval(() => {
secondsLeft -= 1;
render(secondsLeft);
}, 1000);
正确:基于绝对时间差
const endAt = Date.now() + 25 * 60 * 1000;
setInterval(() => {
const secondsLeft = Math.max(0, Math.round((endAt - Date.now()) / 1000));
render(secondsLeft);
if (secondsLeft === 0) trigger();
}, 250);
第二种写法即使被节流到 1 秒触发一次,每次都重新计算”目标时间 - 现在时间”,回到前台立刻显示正确剩余。这是任何网页倒计时的标准实现。
电脑休眠:好消息和坏消息
好消息:休眠时 JS 引擎暂停,Date.now() 也暂停;唤醒后 Date.now() 直接跳到当前时间。如果计时器是”目标时间 - 当前时间”模式,唤醒后立刻发现已经到点,触发回调。
坏消息:休眠期间什么都不会响——你想让烤箱在睡梦里准点提醒你,浏览器做不到。
手机锁屏比电脑休眠更激进:
- iOS Safari:锁屏约 30 秒后完全冻结 JS 上下文;屏幕亮起会恢复但时间已经跳过。
- Android Chrome:可以多撑一会儿,但 1-2 分钟后也会被系统压制。
结论:超过 1 分钟的倒计时,永远不要假设手机可以锁屏。要么屏幕保持常亮、要么用系统闹钟。
铃声延迟与不响
到点了但铃声没响——99% 是这两个原因:
原因 1:首次解码延迟
mp3 / wav 文件第一次播放时浏览器要解码,耗时几十毫秒到几百毫秒。正确做法:用户点”开始”那一刻就调一次 audio.load() 把音频预热,到点 audio.play() 几乎没延迟。
原因 2:自动播放策略
Chrome / Safari 强制要求音频播放必须有”最近的用户交互”。25 分钟前你点了”开始”,对浏览器来说已经过期——到点时 audio.play() 会被拒绝,抛 NotAllowedError。
对策:
- 用户点”开始”时立刻播一个静音的极短音频(如 1ms 的空 WAV),把当前页面的”音频权限”解锁。
- 到点时优先
audio.play(),捕获 NotAllowedError 立刻发 Notification 兜底。 - 标签页可见时用音频,不可见时用 Notification + 振动(移动端)。
网页关掉了
关掉标签页 = JS 销毁 = 所有计时器作废,没有任何兜底。理论上 Service Worker + Notification Triggers API 可以做到,但浏览器支持飘忽(Chrome 早期实验功能后来撤回),生产环境不能依赖。
如果你需要”关掉网页后还能提醒”——下载一个 .ics 日历事件文件让系统接管,是目前最稳的方案。
计时器累计漂移
跑了 25 分钟,实际是 24:57 或 25:02——这种 ±5 秒以内的漂移完全正常。
漂移来源:
- 浏览器主线程繁忙(动画、滚动、计算)→ setTimeout 回调晚执行
- 多个 tick 各自抖动几十毫秒,累计起来
- 系统时间微调(NTP 同步)
控制漂移的关键:永远用绝对时间差,不用累加。这样每次 tick 抖动多少都不影响最终触发时刻——目标时间到了就触发。
如果你看到”网页倒计时跑了 25 分钟实际却跑了 26 分 30 秒”——99% 是后台节流叠加了累加式实现,前面已经讲过怎么解决。
多人共享倒计时
团建、考试、烹饪比赛——多人各看自己手机倒数,结果终点差 5-10 秒。
根本原因:每人各自点”开始”,开始时刻不同。
正确做法:约定一个”目标时间戳”,所有设备各自倒数到这个时刻:
const targetTime = new Date('2026-05-13T14:25:00+08:00').getTime();
setInterval(() => {
const left = Math.max(0, targetTime - Date.now());
render(left);
}, 100);
即使有人晚 5 秒打开页面、有人切走又回来,所有人都收敛到同一个终点。误差只取决于各设备的系统时钟——多数设备靠 NTP 同步,误差 < 1 秒。
更稳的多人计时是一个大屏 / 主持人手机统一显示,其他人只看不算,彻底消除分歧。
网页计时器 vs 系统闹钟
| 场景 | 选哪个 |
|---|---|
| 番茄钟、专注训练 | 网页计时器(任务期间盯屏幕) |
| 演讲提词、做菜(屏幕常亮) | 网页计时器 |
| 多人共享倒数 | 网页计时器(同步目标时间) |
| 午睡、烤箱、洗衣机(锁屏) | 系统闹钟 |
| > 1 小时长倒计时 | 系统闹钟 |
| 必须响、不能漏(吃药、接送) | 系统闹钟 + 网页计时器双保险 |
网页计时器最大的强项是视觉显示:大屏数字、进度条、多段计时一目了然;最大短板是生命周期受限于标签页。明确边界后用对场景,比纠结”为什么不准”有用得多。
工具内置补偿
倒数模式、正计时秒表、分段计时(lap)、自定义铃声——计时器工具内部使用绝对时间差实现,标签页切走再回来时间不会丢;铃声预解锁、到点时优先 audio + Notification 兜底。但关闭标签页或长时间锁屏的硬限制,浏览器层面绕不过去——这种场景请用系统闹钟。