倒计时精度与服务端时间同步
场景
实现一个秒杀活动倒计时,要求精准、不受浏览器后台 throttle 影响。
面试速答版
精准倒计时怎么做?
以服务端时间为准 + drift 补偿 + 切到前台时校准,不要依赖 setInterval 累加:
- 启动时调
/api/time同步服务端时间,记录 offset(要补偿 RTT/2)。 - 每次 tick 用
Date.now() + offset算剩余,避免累计误差。 - 用
setTimeout+remaining % 1000对齐到整秒,比 setInterval 准。 - 监听
visibilitychange:切回前台立刻校准(后台被 throttle 后会漂)。 - 高精度场景用
requestAnimationFrame或 Web WorkersetInterval不受主线程 throttle 影响。
实现方案
精准倒计时
drift 补偿倒计时
function createCountdown(
endTime: number, // 服务端结束时间(ms 时间戳)
onTick: (remaining: number) => void,
onEnd: () => void
) {
let timer: ReturnType<typeof setTimeout>;
const serverOffset = getServerTimeOffset(); // 服务端与本地的时间差
function tick() {
const now = Date.now() + serverOffset;
const remaining = Math.max(0, endTime - now);
onTick(remaining);
if (remaining <= 0) {
onEnd();
return;
}
// 计算下一次 tick 的延迟:对齐到整秒
const drift = remaining % 1000;
timer = setTimeout(tick, drift || 1000);
}
tick();
return () => clearTimeout(timer);
}
// 服务端时间同步
let _serverOffset = 0;
async function syncServerTime() {
const t1 = Date.now();
const res = await fetch('/api/time');
const t2 = Date.now();
const serverTime = (await res.json()).timestamp;
const rtt = (t2 - t1) / 2;
_serverOffset = serverTime - t2 + rtt;
}
function getServerTimeOffset() { return _serverOffset; }
后台 Tab 补偿
浏览器会将后台 Tab 的定时器 throttle 到最低 1 次/秒甚至暂停。
visibilitychange 补偿
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
// 从后台回来时立即重新计算剩余时间
recalculateCountdown();
}
});
常见面试问题
Q1: 为什么不能用 setInterval(fn, 1000) 做倒计时?
答案:
- 不精确:setInterval 不保证精确间隔,会有累积误差
- 后台 throttle:浏览器后台 Tab 会降低定时器频率
- drift 累积:每次误差几毫秒,长时间后偏差明显
正确做法:每次 tick 时用 Date.now() 重新计算剩余时间,而不是递减计数器。
Q2: 如何同步服务端时间?
答案:
发请求获取服务端时间,同时记录请求的 RTT(往返时间),用 serverTime - localTime + RTT/2 计算偏移量。后续都用 Date.now() + offset 作为"当前时间"。