倒计时为什么会越走越慢:别用 setInterval 累减,改用时间戳

举报
yd_221565968 发表于 2026/11/03 00:26:22 2026/11/03
【摘要】 番茄钟切到后台后倒计时明显变慢。本文分析 setInterval 累减的三个误差来源(后台节流、任务堆积、误差累积),给出用结束时间戳现算剩余时间的写法,并说明暂停恢复、取整方式与 performance.now 的取舍。

番茄钟上线后我发现一个问题:把标签页切到后台去写作业,25 分钟的专注结束时,手机上的真实时间已经过去快 30 分钟了。代码逻辑没错,错在计时方式。

一、错误写法:每秒减一

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

这段代码隐含了一个假设:setInterval 每次回调之间刚好间隔 1000 毫秒。实际上这个假设在三种情况下都不成立。

  • 后台节流:标签页不可见时,浏览器会把定时器的最小间隔拉长到 1 秒以上,移动端甚至可能直接冻结;
  • 任务堆积:主线程忙于渲染或计算时,定时器回调会被推迟,但不会补偿;
  • 误差累积:每次哪怕只慢 5 毫秒,1500 次之后就差了 7 秒多。

后台节流是其中影响最大的。切走几分钟,倒计时可能只走了几十秒。

二、正确写法:记录结束时刻

把“还剩多少秒”从一个需要自己维护的变量,改成每次用系统时间算出来的结果:

let endAt = Date.now() + 25 * 60 * 1000;

const timer = setInterval(() => {
  const left = Math.max(0, Math.round((endAt - Date.now()) / 1000));
  render(left);
  if (left === 0) { clearInterval(timer); onFinish(); }
}, 250);   // 刷新频率只影响画面,不影响计时

这样定时器的职责只剩下“触发一次重绘”。它慢了、漏了几次都没关系,下一次回调算出来的剩余时间依然是准确的。切回前台的瞬间,界面会直接跳到正确的数字。

三、暂停怎么处理

用结束时刻计时后,暂停不能只是停掉定时器,否则恢复时结束时刻已经过期。做法是暂停时把剩余毫秒数存下来,恢复时重新推算结束时刻:

let endAt = 0, remain = 25 * 60 * 1000, running = false;

function start() {
  endAt = Date.now() + remain;   // 从剩余时间续上
  running = true;
}

function pause() {
  remain = Math.max(0, endAt - Date.now());   // 记住还剩多少
  running = false;
}

四、几个配套细节

  • 刷新频率:设成 250 毫秒而不是 1000 毫秒,秒数跳变会更贴近真实时刻,代价可以忽略。
  • 取整方式:用 Math.round 而不是 Math.floor,否则刚启动时会立刻从 25:00 跳到 24:59。
  • 不要依赖回调判断结束:结束条件写成“剩余时间小于等于 0”,而不是“回调执行了 1500 次”。
  • 系统时间被修改:Date.now() 会受用户改系统时间影响。对计时精度要求高的场景,可以改用 performance.now(),它是单调递增的,不受系统时钟调整影响。

五、小结

凡是和真实时间挂钩的功能,倒计时、限时答题、令牌过期,都不应该让定时器来累计时间。定时器只负责“该刷新了”,时间本身永远从时钟现算。记住这一点,后台切换、主线程卡顿都不会再影响结果。

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

评论(0)

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

全部回复

上滑加载中

设置昵称

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

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

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