坑??!前端倒計(jì)時(shí)有誤差!讓公司損失了幾十萬(wàn)!
前幾天聽(tīng)說(shuō)公司某個(gè)項(xiàng)目組,由于不對(duì)定時(shí)器進(jìn)行誤差處理,間接導(dǎo)致?lián)p失了幾十萬(wàn)。。還被領(lǐng)導(dǎo)公開(kāi)批評(píng)了。因?yàn)槎〞r(shí)器誤差,導(dǎo)致了公司幾個(gè)單子處理失敗。。。白白損失了 money。
"倒計(jì)時(shí)誤差"是前端開(kāi)發(fā)中的經(jīng)典問(wèn)題,也是面試官考察候選人問(wèn)題分析能力的高頻考點(diǎn)
一、誤差從何而來(lái)?
核心問(wèn)題: 瀏覽器主線程的阻塞
前端倒計(jì)時(shí)通?;?/span>setInterval
或setTimeout
實(shí)現(xiàn),而這兩個(gè)API存在以下問(wèn)題:
- 時(shí)間間隔不可靠:實(shí)際執(zhí)行間隔 >= 設(shè)定值
- 事件循環(huán)被阻塞:同步代碼、長(zhǎng)任務(wù)、網(wǎng)絡(luò)請(qǐng)求等
- 標(biāo)簽頁(yè)休眠:瀏覽器后臺(tái)運(yùn)行時(shí)計(jì)時(shí)器會(huì)被限流(最小延遲4ms甚至更長(zhǎng))
- 系統(tǒng)休眠:電腦睡眠/鎖屏?xí)?dǎo)致計(jì)時(shí)完全暫停
// 典型的誤差累計(jì)示例
let count = 10
const timer = setInterval(() => {
count--
console.log(count)
}, 1000)
// 實(shí)際誤差會(huì)隨時(shí)間逐漸增大
二、五大解決方案
1. 動(dòng)態(tài)校準(zhǔn)法(基礎(chǔ)版)
function accurateTimer(fn, interval) {
let expected = Date.now() + interval
let timeout
const tick = () => {
const drift = Date.now() - expected // 計(jì)算偏差
fn()
expected += interval // 更新下次預(yù)期時(shí)間
timeout = setTimeout(tick, Math.max(0, interval - drift))
}
timeout = setTimeout(tick, interval)
return() => clearTimeout(timeout)
}
- 優(yōu)點(diǎn): 簡(jiǎn)單有效
- 缺點(diǎn): 無(wú)法解決系統(tǒng)休眠等長(zhǎng)時(shí)間阻塞
2. Web Worker 計(jì)時(shí)
將計(jì)時(shí)器移至獨(dú)立線程,避免主線程阻塞
// main.js
const worker = new Worker('timer.worker.js')
worker.postMessage({ cmd: 'start', interval: 1000 })
// timer.worker.js
self.addEventListener('message', (e) => {
setInterval(() => {
self.postMessage('tick')
}, e.data.interval)
})
- 優(yōu)點(diǎn): 隔離主線程影響
- 缺點(diǎn): 仍受系統(tǒng)休眠影響
3. 服務(wù)器時(shí)間同步
通過(guò)接口獲取服務(wù)器時(shí)間進(jìn)行校準(zhǔn)
async function syncTime() {
const start = Date.now()
const res = await fetch('/api/time')
const serverTime = await res.json()
const latency = Date.now() - start
return serverTime + Math.floor(latency/2) // 假設(shè)網(wǎng)絡(luò)延遲對(duì)稱
}
最佳實(shí)踐:
- 首次加載時(shí)校準(zhǔn)
- 定時(shí)輪詢同步(例如每1分鐘)
- 使用 WebSocket 保持時(shí)間同步
4. Performance API 高精度計(jì)時(shí)
使用performance.now()
獲取亞毫秒級(jí)時(shí)間戳
const start = performance.now()
function check() {
const elapsed = performance.now() - start
if(elapsed >= 1000) {
// do something
start = performance.now()
}
requestAnimationFrame(check)
}
特點(diǎn):
- 精度可達(dá)微秒級(jí)
- 不受系統(tǒng)時(shí)間修改影響
- 適合需要高精度計(jì)算的場(chǎng)景
5. 頁(yè)面可見(jiàn)性API優(yōu)化
在標(biāo)簽頁(yè)不可見(jiàn)時(shí)降低更新頻率
document.addEventListener('visibilitychange', () => {
if(document.hidden) {
clearInterval(timer)
// 記錄剩余時(shí)間
} else {
// 重新校準(zhǔn)后啟動(dòng)
}
})
三、生產(chǎn)環(huán)境最佳實(shí)踐
根據(jù)場(chǎng)景選擇組合方案:
場(chǎng)景 | 推薦方案 | 誤差控制 |
秒殺倒計(jì)時(shí) | 服務(wù)器時(shí)間 + WebSocket + 動(dòng)態(tài)校準(zhǔn) | <100ms |
問(wèn)卷計(jì)時(shí) | Web Worker + 本地存儲(chǔ)恢復(fù) | <1s |
動(dòng)畫倒計(jì)時(shí) | RAF + 性能監(jiān)測(cè) | 16ms以內(nèi) |