倒计时精度与服务端时间同步
场景
实现一个秒杀活动倒计时,要求精准、不受浏览器后台 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 作为"当前时间"。