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

你有没有想过,为什么手机自带的秒表用了十年都没出过问题,而网页里的计时器动不动就"偷时间"?

一位前端工程师最近栽了个跟头。他以为10分钟能搞定的浏览器秒表,硬是写了两天。问题不在代码量,而在浏览器里藏着一个你从未察觉的"摸鱼机制"——当你切到别的标签页,它真的会偷懒。

10毫秒的承诺,14毫克的现实

10毫秒的承诺,14毫克的现实

最直觉的写法长这样:每隔10毫秒加10,看起来精准得像瑞士钟表。但浏览器会告诉你:图样图森破。

setInterval(定时器)保证的是"最少等10毫秒",不是"正好10毫秒"。事件 loop 忙不忙、CPU 有没有在渲染别的页面、垃圾回收是不是正在扫内存——这些都会让实际间隔变成14到20毫秒。跑满60秒,误差能攒出好几秒。用户看着表显示1分钟,实际已经过了1分03秒。

这种漂移不是bug,是设计如此。JavaScript 是单线程的,它没法像硬件中断那样强行插队。

工程师的第一次觉醒:别数心跳,看墙上的钟。

换成 performance.now()(高精度时间戳)记录开始时刻,每次刷新时现算 elapsed = 现在 - 开始。这个时间戳跟系统时钟无关,调时区、改时间都不会让它跳表。Date.now() 虽然也能用,但精度只有毫秒,而且系统时间一变它就跟着变——想象一下秒表突然从30秒跳到2小时。

显示刷新也换了 requestAnimationFrame(请求动画帧),跟屏幕刷新率绑定,通常是60Hz。既够肉眼看了,又不用疯狂操作 DOM。

切个标签页,时间凭空消失

切个标签页,时间凭空消失

但真正的坑在下一步。浏览器有个"节能正义":标签页切到后台,定时器会被疯狂节流,setInterval 可能1秒才响一次;requestAnimationFrame 更彻底,直接暂停。

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

对用户来说这是什么体验?开始计时,切去回个微信,回来一看——秒表还在走,但比真实时间慢了一截。不是卡了,是浏览器觉得"反正你看不见,我歇会儿"。

这个设计本意是好的。后台标签页如果还跟前台一样勤快,笔记本电池撑不过两小时。但对秒表这种需要墙钟精度的场景,它就是灾难。

工程师的第二次觉醒:得让页面"睡醒"时自己补课。

visibilitychange(可见性变化)事件派上用场。标签页重新可见时,立刻用那个从未变过的 startTime 重新计算 elapsed,显示瞬间追上。用户看到的不是"从卡住的地方继续",而是"时间原来已经走了这么远"。

圈速功能:简单需求,隐藏复杂度

圈速功能:简单需求,隐藏复杂度

基础计时搞定后,工程师加了圈速(lap)功能——每按一下记录当前时刻,同时显示这一圈比上一圈快多少。

实现本身不复杂:数组存历史记录,当前减上一个就是分段用时。但 UX 设计暴露了新问题:用户要同时看到总时间和分段差值,而且分段可能是负数(这一圈比上一圈慢)。

这里没用什么高深算法,就是状态管理。但工程师意识到,一个"能用"的秒表和"好用"的秒表,差距往往在这些细节。

整个项目最后交付时,代码量其实不大。两天时间主要花在理解浏览器的行为边界,以及测试各种边缘场景——低电量模式、后台标签、系统休眠恢复。

浏览器不是操作系统,它是个在操作系统之上、还要讨好电池和用户体验的中间层。你想让它像硬件一样可靠,就得知道它在哪些地方会选择性不可靠。

这位工程师把经历写成了博客,评论区最高赞是:"所以我用了十年手机自带秒表,从没想过网页版为什么这么少。"另一条回复更扎心:"Chrome 团队看到这篇文章,默默把后台节流从1秒改成了2秒。"