高精度倒计时应基于performance.now()单调时钟和requestanimationframe实现,避开date.now()偏差与setinterval漂移;启动时记录starttime和targetperf=starttime+durationms,每帧用raf回调timestamp计算remaining=targetperf-timestamp,并解耦计算与渲染。

高精度倒计时的关键不是“更频繁地调用 setInterval”,而是**避开 Date.now() 的采样偏差 + 定时器固有漂移**,转而基于**单调时钟(monotonic clock)+ 时间差校准**来实现。现代浏览器中,performance.now() 提供微秒级、高分辨率、不受系统时间调整影响的单调时间源,是唯一可靠起点。
用 performance.now() 做基准时间锚点
performance.now() 返回自页面加载以来的毫秒数(实际精度通常达微秒级),它不依赖系统时钟,不会因 NTP 同步或用户手动改时间而跳变,是真正的“流逝时间”度量。倒计时应从这一刻开始计算理论剩余时间,而非反复读取 Date.now()。
- 启动倒计时时,记录当前
performance.now()为startTime,同时记录目标毫秒时间戳(如targetMs = Date.now() + durationMs) - 后续每一帧(requestAnimationFrame)中,计算已过去时间:
elapsed = performance.now() - startTime - 剩余时间 =
Math.max(0, targetMs - Date.now() + elapsed - (performance.now() - startTime))→ 实际简化为:剩余时间 = targetMs - Date.now() + elapsed - (performance.now() - startTime) = targetMs - Date.now()?不对 —— 这会回到 Date 误差。正确做法是:完全放弃 Date.now() 做减法,只用 performance.now() 推演 - 更稳健方案:把目标也转成 performance 时间基准 ——
targetPerf = startTime + durationMs,之后所有计算只用remaining = Math.max(0, targetPerf - performance.now())
用 requestAnimationFrame 替代 setInterval
setInterval(fn, 16) 无法保证每 16ms 执行一次,受 JS 主线程阻塞、浏览器节流(后台标签页)、系统调度延迟影响,累积误差可达数十毫秒/秒。而 requestAnimationFrame 与屏幕刷新节奏同步(通常 60fps ≈ 16.67ms),且在页面可见时更稳定,更重要的是 —— 它天然支持“上一帧到下一帧的实际耗时”反馈。
- 每次 rAF 回调接收一个
DOMHighResTimeStamp参数(即当前 performance.now() 值),这是最准的“此刻”时间 - 无需自己维护 startTime,直接用回调参数计算剩余:
remaining = Math.max(0, targetPerf - timestamp) - 当 remaining ≤ 0 时停止 rAF,避免超调
补偿系统休眠或页面非活跃导致的时间跳变
用户切到其他标签页、电脑睡眠再唤醒,performance.now() 仍连续增长,但 rAF 会暂停。唤醒后第一帧的 timestamp 可能比上一帧大几百甚至几千毫秒 —— 这意味着倒计时必须“跳过”这段挂起时间,否则显示卡住或突变。
- 记录上一帧时间
lastTime,当前帧得delta = timestamp - lastTime - 若
delta > 1000(比如超过1秒),说明发生了明显挂起,应将targetPerf向前推进delta,或直接重置为timestamp + remaining(保持逻辑连续) - 更稳妥做法:不依赖 delta 累加,始终用
remaining = targetPerf - timestamp,只要targetPerf是基于初始 performance.now() 设定的,它本身已隐含了挂起时间 —— 唤醒后timestamp突增,remaining自动减少,无需额外补偿
渲染与时间分离:避免 display 更新拖慢逻辑
倒计时数字重绘(尤其是 DOM 操作或复杂格式化)可能耗时,若放在 rAF 里执行,会挤压下一次 rAF 的可用时间,导致节奏变慢。应把时间计算和 UI 更新解耦。
- 在 rAF 回调中只做纯计算:
remaining = Math.max(0, targetPerf - timestamp),并更新内部状态 - 用独立的、低频的定时器(如 50ms setInterval)或
queueMicrotask批量更新视图,避免每帧都操作 DOM - 对精度要求极高(如毫秒级动画同步)时,可结合 CSS
@property和transition,让剩余时间驱动 transform 或 opacity,由 GPU 渲染,彻底脱离 JS 主线程瓶颈











