序言
你踩過嗎?瀏覽器節能機制導致Websocket斷連的坑~~~
近期,在使用WebSocket(WS)
連接時遇到了頻繁斷連的問題,這種情況在單個用戶上每天發生數百次。盡管利用了socket.io
的自動重連機制能夠在斷連后迅速恢復連接,但這并不保證每一次重連都能成功接收WS
消息。因此,我們進行了一些的排查和測試工作。
最終發現問題的根本原因:正是瀏覽器的節能機制,不經意間成為了這一問題的幕后黑手。
瀏覽器節能機制簡介
瀏覽器的節能機制逐漸成為前端開發者需要關注的問題。特別是這些節能機制可能會對定時器的精度產生影響,這直接關系到前端應用的用戶體驗,在某些場景下甚至影響到用戶的使用。
為了減少電能消耗,提高電池續航能力,現代瀏覽器都引入了節能機制。這些機制包括但不限于降低空閑標簽頁的CPU
使用率、減少后臺JavaScript
的執行頻率、限制定時器的精確度等。雖然這些措施顯著提高了設備的能效,但也給前端開發帶來了一些挑戰。
WS頻繁斷連原因分析
查閱socket.io官網服務端配置的pingTimeout
和pingInterval
兩個參數發現WS心跳異常時會導致重連,具體說明:
WS連接中服務端和客戶端兩端必須一直保持心跳。如果有一端停止,則滿足如下條件之一就會自動斷連:
看文檔發現其實高版本的socket.io是由服務端定時發起ping。而在socket.io 2.X的版本中內置的心跳機制是由客戶端定時發起。而瀏覽器在后臺運行時,即使你設置了一個每秒觸發的定時器,它也只能每分鐘觸發一次,超過了pingInterval + pingTimeout
設置的時間,最后看到的日志是很有規律的每分鐘重連一次。在之前寫的這篇文章中也有相關的介紹《掌握Web Workers:徹底解鎖前端多線程編程的潛力》
WS頻繁斷連解決方法
@升級socket.io到最新版本
上面的截圖其實就是最新版本(4.x)的,升級后由服務器定時發起心跳。在服務端定時運行,避開了瀏覽器節能機制對定時器的影響
@自定義WS心跳事件
為了減小直接升級對已有業務的影響,目前使用的也是這種方案:在服務端自定義心跳事件,定時發送心跳custom-ping
// 客戶端的CODE
io.on('custom-ping', function () {
io.emit('custom-pong', Date.now())
})
// 服務端CODE
io.on('connection', (socket) => {
console.log('New client connected');
// 發送自定義ping消息
const pingInterval = setInterval(() => {
socket.emit('custom-ping', Date.now());
}, 10000); // 每10秒發送一次
// 監聽自定義pong消息
socket.on('custom-pong', (data) => {
console.log('Pong received:', data);
});
socket.on('disconnect', () => {
clearInterval(pingInterval);
console.log('Client disconnected');
});
});
注意:斷連時一定要銷毀定時器
其實,socket.io是有內置心跳的(2.x版本客戶端定時發起,4.x由服務端定時發起),自定義心跳的意義主要在于保持數據交換,在這個時間間隔內保持數據交換,socket就不會自動中斷重連。
@使用setTimeout
這里要注意使用setTimeout的姿勢,如果是直接這樣使用、依然會有精度問題。
setTimeout丟失精度的情況:
// 以下setTimeout仍然會丟失精度
let _cacheTs = Date.now()
const _setTimeoutFn = () => {
console.log('setTimeout :>> ', Date.now() - _cacheTs);
_cacheTs = Date.now()
setTimeout(() => {
_setTimeoutFn()
}, 5000)
}
_setTimeoutFn()
在setTimeout里面去執行一個函數棧會被瀏覽器監控到,會認為和setInterval一樣,其在后臺運行時會降低其定時精度。 但如果這樣可以避開節能機制的限制:
setTimeout不丟失精度的情況:
// 客戶端CODE
// 監聽服務端發送的custom-pong事件
socket.on('custom-pong', onHeart)
const onHeart = () => {
if (timer) {
clearTimeout(pingTime.current)
}
timer = window.setTimeout(() => {
socket.emit('custom-ping', Date.now())
}, 5000)
}
// 服務端CODE
socket.on('custom-ping', ()=>{
socket.emit('custom-pong', Date.now())
})
@使用Web-Workers
在Web-Workers線程內發起定時不受瀏覽器節能機制的限制,相關示例在這篇文章里也有介紹《掌握Web Workers:徹底解鎖前端多線程編程的潛力》
@頁面保活(實測無效)
在后臺運行時也保持瀏覽器的活躍,用得最多的方式是在頁面隱藏一個循環播放的音頻 或者 使用nosleep.js
const noSleepInstance = new NoSleep();
document.addEventListener('click', function enableNoSleep() {
document.removeEventListener('click', enableNoSleep, false);
noSleepInstance.enable();
}, false);
實測,使用這種方式時,瀏覽器在后臺運行仍然存在定時器精度降低的問題。
小結
WS頻繁斷連的原因:
使用了低版本(2.x)的socket.io
在客戶端每5秒定時發送 心跳
瀏覽器后臺運行時觸發節能機制限制了定時器的精度,由每5秒變成了實際的每分鐘執行一次
每分鐘執行一次遠大于socket.io設置的pingTimeout時間
WS斷開連接
socket.io內置的重連機制,立即重連成功
查看日志發現每分鐘重連一次。
在實際排查中,是從第七步倒退排查發現是瀏覽器節能機制所引起的問題。。。
總結
作者:tager
鏈接:https://juejin.cn/post/7362576319928008755
來源:稀土掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。