使用方法
- 页面打开时默认是「按持续时长」模式,时长框里是 0 时 5 分 0 秒,大字区显示
00:05:00,与下面「完整示例」和对照表是同一个值。 - 倒计时固定长度就在小时、分钟、秒三个框里填,也可以点「1 分钟 / 5 分钟 / 10 分钟 / 1 小时」快捷按钮。
- 盯着绝对时刻就把模式切到「按指定时刻」,只填
HH:MM(如20:00):按本地时区解释,今天已过则自动算明天,并在提示里说明。 - 点「开始 / 继续」或在任意输入框按
Enter,目标时刻被记成绝对时间戳,结果卡同时显示本地与 UTC 时间;「暂停」冻结剩余,继续时按新的目标时刻重算。 - 诊断行会显示定时器触发次数与墙钟偏差。切到别的标签页时定时器被清掉,回到本页按当前时间重算再继续刷新,所以后台待多久都不会多走或少走。
这个倒计时器能解决什么问题
一是给固定时长掐表。煮咖啡、限时发言、热身拉伸要的是「从现在起 N 分钟」,默认 5 分钟档直接可用,快捷按钮覆盖 1 分钟到 1 小时。
二是盯着一个绝对时刻。会议 20:00 开始、车次 08:15 发车,要的不是「还有多久」而是「到某个定点」。本工具支持只填 HH:MM,把目标时刻记成时间戳,剩余量由它减当前时间得出。
三是长时间挂后台也不失准。浏览器会给后台标签页的定时器限速,很多网页倒计时因此越跑越慢;本工具不靠触发次数计秒,切回前台按当前时间重算,挂 8 小时和挂 5 分钟一样准。
四是把「为什么会越走越慢」讲清楚。诊断行实时显示触发次数与墙钟偏差,原理一节给出累加式的实测漂移表,两处对照就能理解为什么「每次加一秒」是错的。
本页不做这些事:不做从零开始正向累加(→在线计时器);不做分段计次与圈速(→在线秒表);不做「工作 25 分钟 + 休息 5 分钟」的循环(→番茄钟计时器);不做到点反复响铃(→闹钟);也不做「跑 30 秒歇 15 秒」的间歇循环(→间隔计时器HIIT)。
为什么「每次加一秒」会越走越慢:setInterval 漂移与绝对时间基准
这个倒计时器内部只有一个减法:remain = Math.max(0, toEnd - Date.now())。toEnd 是开始那一刻算出的绝对目标时间戳,之后每次刷新都用它减当前时间。常见的错误写法是反过来的:setInterval(function(){ left -= 1000 }, 1000),靠触发次数计秒。规范只保证 setInterval 的间隔不小于 1000 毫秒,回调排队、渲染、垃圾回收都会插在中间,设额外开销为 d 毫秒,真实周期就是 1000 + d,累加式在墙钟走过 T 毫秒后只触发 floor(T / (1000 + d)) 次。d 取 1 毫秒,1 小时后就慢 4 秒、8 小时慢 29 秒;d 取 5 毫秒则是 18 秒与 144 秒。绝对时间基准与触发次数无关,偏差恒为 0,唯一可感知的误差是 200 毫秒的显示刷新粒度,且不累积。
后台节流会把这笔账放大几十倍。按 Chrome 现行策略,标签页隐藏后 setInterval 最快也只能 1 秒一次,隐藏满 5 分钟进入 intensive throttling,最多 1 分钟一次。累加式因此断崖式欠账:隐藏 30 分钟只触发 325 次,显示成 00:05:25;隐藏 8 小时只触发 775 次,显示 00:12:55,而墙钟早已走完。绝对时间基准回到前台的第一次刷新就会修正。本页更省一步:页面一隐藏就调用 clearInterval,不在后台空转。可见性判据用 W3C Page Visibility 规范定义的 document.hidden 与 visibilitychange,比监听 window 的 blur / focus 可靠——切换应用、锁屏、最小化都会正确改变可见性;事件必须挂在 document 上,挂到 window 在部分浏览器收不到。
Date.now() 与 performance.now() 是两套时钟。前者返回自 1970-01-01T00:00:00Z 起的 UTC 毫秒,是墙钟,系统改时间与 NTP 校时都会让它跳变;后者是从页面导航起算的单调时钟,单位毫秒但带小数,且被浏览器降精度到微秒级,不受系统时间调整影响。要跟墙上时间对齐(到某个时刻归零、跨刷新仍成立)必须用前者;只测量「经过了多久」(性能打点)应该用后者。
跨时区与 UTC 陷阱。时间戳本身是 UTC 毫秒,「还剩多少毫秒」在任何时区都一致,会出错的是解析:ECMAScript 规定 2026-10-01 这种纯日期字符串按 UTC 解析,带时间的 2026-10-01T00:00 按本地时间解析,在东八区两者相差 8 小时。本页输入的是 HH:MM,构造目标时刻时显式传入年、月、日、时、分,走本地时间分支,不会踩到这条规则;结果卡同时给出本地与 UTC 时间,方便跨时区核对。另外 Date.now() 不受夏令时影响,但「今天 20:00」这个本地时刻在夏令时切换日可能不存在或出现两次——中国大陆自 1991 年起不再实行夏令时,这个坑主要影响海外用户。
完整示例(数值仅作演示)
这个倒计时器的示例条件与页面默认值完全一致:按持续时长模式,0 时 5 分 0 秒,即 300 秒、300000 毫秒。
- 点「开始」后目标时刻记为「当前时间 + 300000 毫秒」,大字区显示
00:05:00,按 200 毫秒刷新一次。 - 同一段 5 分钟,累加式只要回调平均多花 5 毫秒就慢 2 秒,多花 10 毫秒慢 3 秒。
- 换成 8 小时,累加式在额外开销 5 毫秒时慢 144 秒、10 毫秒时慢 286 秒;绝对时间基准仍是 0。
- 把页面切到后台 8 小时,累加式只收到 775 次触发,显示停在
00:12:55,比实际慢 7 小时 47 分 5 秒;绝对时间基准回到前台立刻归零。 - 诊断行的数字可自查:墙钟毫秒数减去「触发次数 × 200」,就是定时器晚出来的量,它不会进入剩余时间。
这些数字都由「回调平均多花 d 毫秒」这个模型推出,d 取别的值时见下一节对照表;真实环境里 d 会波动,但方向永远是「越走越慢」。
漂移与时钟对照表
下表按 floor(T / (1000 + d)) 逐项算出,「慢多少」= 墙钟秒数 − 累加式显示秒数;误差随时间线性累积,8 小时的误差约为 1 小时的 8 倍。
| 模型 | 1 小时后显示 | 1 小时慢多少 | 8 小时后显示 | 8 小时慢多少 |
|---|---|---|---|---|
| 累加式,额外开销 d = 1 ms | 3596 秒 | 4 秒 | 28771 秒 | 29 秒 |
| 累加式,额外开销 d = 2 ms | 3592 秒 | 8 秒 | 28742 秒 | 58 秒 |
| 累加式,额外开销 d = 5 ms | 3582 秒 | 18 秒 | 28656 秒 | 144 秒 |
| 累加式,额外开销 d = 10 ms | 3564 秒 | 36 秒 | 28514 秒 | 286 秒 |
| 绝对时间基准(本页) | 3600 秒 | 0 秒 | 28800 秒 | 0 秒 |
下表假设倒计时一开始页面就被隐藏,按「前 5 分钟每秒一次、之后每分钟一次」推导累加式收到的触发次数:
| 页面隐藏时长 | 累加式触发次数 | 累加式显示 | 墙钟实际 | 慢多少 |
|---|---|---|---|---|
| 5 分钟 | 300 次 | 00:05:00 | 00:05:00 | 0 秒 |
| 30 分钟 | 325 次 | 00:05:25 | 00:30:00 | 24 分 35 秒 |
| 1 小时 | 355 次 | 00:05:55 | 01:00:00 | 54 分 5 秒 |
| 8 小时 | 775 次 | 00:12:55 | 08:00:00 | 7 小时 47 分 5 秒 |
最后并排列出两套时钟的适用场景,选错时钟是同类工具里最常见的隐性 bug:
| 取时方式 | 时钟类型 | 粒度 | 受系统改时间影响 | 适合用来做 |
|---|---|---|---|---|
| Date.now() | 墙钟(UTC 毫秒) | 1 ms | 会跳变 | 与墙上时间对齐的倒计时、跨刷新持久化 |
| performance.now() | 单调时钟 | 微秒级 | 不受影响 | 测量「经过了多久」、性能打点 |
| new Date().getTime() | 与 Date.now() 等价 | 1 ms | 会跳变 | 同上,写法更啰嗦,一般不推荐 |
输入范围、边界与精度说明
- 这个倒计时器的时长上限是 99 时 59 分 59 秒,即 359999 秒。这也避开了定时器的硬边界:
setTimeout与setInterval的延时按 Web IDL 的 32 位有符号整数传递,最大 231 − 1 = 2147483647 毫秒(约 24.86 天),超过会被按溢出值处理、可能立即触发。 - 目标是过去时刻:「按指定时刻」只填时与分、不填日期,今天该时刻已过就按明天同一时刻算;填成
25:00或20:70会被拒绝,并指出小时 00~23、分钟 00~59 的范围。 - 时长小于等于 0 会被拒绝:三个框全填 0 时给出「至少填 1 秒」的具体提示,不做静默归零——静默归零会让「点了开始却什么都没发生」变成难查的问题。
- 刷新 200 毫秒、显示向上取整:剩余 299999 毫秒显示
00:05:00而不是00:04:59,避免「还没到就少一秒」的错觉,只有真正归零才显示00:00:00。 - 暂停会迁移目标时刻:暂停时把剩余冻结成数字并清掉定时器,继续时用「当前时间 + 冻结剩余」重算目标时刻。所以暂停期间改动系统时间,恢复后一律以当前时间为准。
- 系统时间被改动或电脑睡眠:绝对基准依赖
Date.now(),手动改时间与 NTP 校时都会让它跳变,倒计时随之变长或变短,这正是「对齐墙上时间」应有的行为;合盖睡眠时页面不执行但系统时钟继续走,回到前台第一次刷新就把剩余算对。 - 跨时区与关闭页面:结果卡同时给出本地与 UTC 时间,把截止点告诉别的时区的人请直接发 UTC 那行,不要只发「20:00」而不带时区;关闭标签页后定时器随页面销毁,不会后台响铃。
- 定时器的清理可验证:每次暂停、重置、归零都会调用
clearInterval并把句柄置空,页面隐藏时也清理;点「暂停」后诊断行立刻停止增长,就说明不再有回调在跑。
常见问题
切到别的标签页,倒计时会停吗?回来后还准吗?
不会停也不会变慢。页面隐藏时本页主动 clearInterval(不在后台空转),回到前台先算一次 toEnd - Date.now() 再重建定时器。剩余量来自两个绝对时间戳,隐藏多久都不影响结果。
为什么「每次减一秒」的写法会越跑越慢?
setInterval 只保证间隔不小于 1000 毫秒,回调排队让真实周期略大,累加式把「触发一次」当成「过了一秒」,日积月累就欠账。按 5 毫秒额外开销算,1 小时欠 18 秒、8 小时欠 144 秒;切到后台后半小时就能欠出 24 分多钟。
Date.now() 和 performance.now() 该用哪个?
看要对齐哪种时间:要「到某个墙上时刻归零」用 Date.now(),要「量出经过多少毫秒」用 performance.now()(单调时钟,系统改时间不影响它)。本页做的是前者,所以固定用 Date.now()。
电脑自动对时或者我手动改了系统时间,会怎样?
倒计时会跟着系统时间走:往前调剩余变大,往后调变小甚至立刻归零。这是「与墙上时间对齐」的必然结果,不是 bug。NTP 校时通常只有几十毫秒微调,肉眼看不出来。
跨时区的人一起看同一个截止时间,怎么保证一致?
用结果卡里的 UTC 那行。时间戳是 UTC 毫秒,任何时区算出的「还有多久」都相同,会不一致的只有本地化显示。另外 new Date('2026-10-01') 按 UTC 解析、带时间的 2026-10-01T00:00 按本地解析,在东八区差 8 小时。
页面关掉还会响吗?
不会。定时器依附当前页面,关闭标签页就一起销毁。需要关机后仍然生效的提醒请用系统日历;本页适合「人还在电脑前、要盯一个时长或一个时刻」的场景。
为什么倒计时到 0 会晚一点点?
本页每 200 毫秒刷新一次,归零最多晚 0.2 秒显示;内部剩余量一直按绝对时间算,不会因为显示延迟而多算或少算。把间隔调到 1000 毫秒能省开销,但秒数跳动会明显滞后。
我设了 8 小时,中途合盖睡眠了,醒来还准吗?
准。睡眠期间页面不执行但系统时钟在走,醒来后的第一次刷新就得到正确剩余量,通常已归零并提示时间到。虚拟机挂起这类「系统时钟也停」的情况才会偏慢,那是墙钟方案的固有边界。