前端倒计时为什么会越走越慢?用时间戳重写计时器

举报
yd_221565968 发表于 2026/10/31 21:29:57 2026/10/31
【摘要】 番茄钟切到后台标签页后计时严重偏慢,根因是把 setInterval 当秒表、靠累加计算剩余时间。本文解释后台限流与主线程阻塞导致的误差累积,并给出以结束时间戳为准的重写方案,以及暂停恢复、visibilitychange 刷新等细节处理。

番茄钟上线后我自己用了几天,发现一个尴尬的问题:切到别的标签页写作业,二十多分钟后回来,计时器还剩七八分钟没走完。明明设的是 25 分钟,实际却拖了半个多小时。排查下来,问题出在计时方式上。

一、错误写法:把 setInterval 当秒表

let left = 25 * 60;
setInterval(() => {
  left--;          // 每次假设正好过了 1 秒
  render(left);
}, 1000);

这段代码隐含了一个假设:每次回调之间正好相隔 1 秒。但这个假设在浏览器里并不成立,原因有两个。

  • 后台标签页被限流:页面不可见时,浏览器会把定时器的最小间隔拉长到 1 秒甚至更久,省电策略更激进的时候间隔会更长。于是真实时间走了 30 分钟,回调可能只执行了 1200 次。
  • 主线程阻塞会顺延:setInterval 只保证“不早于”指定时间触发。页面在渲染长列表或执行耗时脚本时,回调会被推迟,误差会一次次累积。

也就是说,用累加的方式计时,误差只会越积越多,不会自己修正。

二、正确写法:只信时间戳

思路很简单:不要去数“过了多少次 1 秒”,而是在开始时记下结束时刻,之后每次刷新都用当前时间重新算剩余秒数。

let endAt = 0;          // 结束时刻的时间戳
let timer = null;

function start(seconds) {
  endAt = Date.now() + seconds * 1000;
  tick();
  timer = setInterval(tick, 250);   // 刷新比 1 秒更密,减少跳秒
}

function tick() {
  const left = Math.max(0, Math.round((endAt - Date.now()) / 1000));
  render(left);
  if (left === 0) {
    clearInterval(timer);
    timer = null;
    onFinish();
  }
}

这样一来,定时器只负责“触发刷新”,不负责“计量时间”。即使回调被推迟,甚至几十次没有执行,下一次醒来算出的剩余时间依然是准确的。我把刷新间隔设成 250 毫秒,是为了让秒数跳变看起来更自然,代价可以忽略。

三、暂停和继续

既然记录的是结束时刻,暂停时就要把它换算回剩余秒数,恢复时再重新计算:

let pausedLeft = 0;

function pause() {
  clearInterval(timer);
  timer = null;
  pausedLeft = Math.max(0, Math.round((endAt - Date.now()) / 1000));
}

function resume() {
  start(pausedLeft);   // 用剩余秒数重新计算结束时刻
}

四、几个容易忽略的细节

  • 回到前台时立即刷新:监听 visibilitychange,页面重新可见时主动调用一次 tick,避免用户先看到一个过期的旧数字。
  • 用户改系统时间:Date.now() 会跟着跳变。如果要防这种情况,可以改用 performance.now(),它是单调递增的,不受系统时钟影响。对番茄钟来说一般没必要。
  • 取整用 round 而不是 floor:用 floor 会让计时器一启动就显示少 1 秒,观感上像是丢了一秒。
  • 结束提醒要有兜底:如果标签页在后台,弹窗和提示音可能要等回到前台才生效。可以在 tick 里判断剩余时间已经为 0,补一次提醒。

五、小结

前端做任何计时功能,原则都是一样的:定时器用来驱动刷新,时间一律从时间戳算。凡是把 setInterval 当秒表、靠累加得出结果的写法,在后台标签页或繁忙页面里都会失准。改完之后,我再切出去写作业,回来时番茄钟的剩余时间终于对得上了。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。