Preface
Recently, while using WebSocket (WS) connections, we encountered frequent disconnection issues, occurring hundreds of times per day for a single user. Although using the auto-reconnect feature of socket.io allowed us to quickly restore connections after disconnections, it did not guarantee that every reconnection would successfully receive WS messages. Therefore, we conducted several investigations and tests.
Ultimately, we identified the root cause of the issue: the browser's power-saving mechanism, which inadvertently became the culprit behind the problem.
Overview of Browser Power-Saving Mechanisms
The power-saving mechanisms of browsers are becoming an increasingly important consideration for front-end developers. These mechanisms can particularly impact the precision of timers, directly affecting the user experience of front-end applications, and in some cases, even influencing usability.
To reduce power consumption and extend battery life, modern browsers have introduced power-saving mechanisms. These include, but are not limited to, reducing CPU usage for idle tabs, decreasing the execution frequency of background JavaScript, and limiting the precision of timers. While these measures significantly enhance device efficiency, they also present some challenges for front-end development.
Analysis of Frequent WS Disconnections
Upon reviewing the pingTimeout
and pingInterval
parameters in the socket.io server configuration, we discovered that abnormal WS heartbeats lead to reconnections. Here's a detailed explanation:
pingInterval
Default value: 25000
This value is used in the heartbeat mechanism, which periodically checks if the connection is still alive between the server and the client.
The server sends a ping packet every pingInterval ms, and if the client does not answer with a pong within pingTimeout ms, the server considers that the connection is closed.
Similarly, if the client does not receive a ping packet from the server within pingInterval + pingTimeout ms, then the client also considers that the connection is closed.
In both cases, the disconnection reason will be: ping timeout
socket.on("disconnect", (reason) => { console.log(reason); // "ping timeout" });
CAUTION
Using a small value like 1000 (one heartbeat per second) will incur some load on your server, which might become noticeable with a few thousands connected clients.
pingTimeout
Default value: 20000
See above.
CAUTION
Using a smaller value means that a temporarily unresponsive server might trigger a lot of client reconnections.
On the contrary, using a bigger value means that a broken connection will take longer to get detected (and you might get a warning on React Native if
pingInterval
+pingTimeout
is bigger than 60 seconds).
In a WS connection, both the server and client must maintain a constant heartbeat. If either side stops, the connection will automatically disconnect if either of the following conditions is met:
- The server sends a ping, and if the client does not respond with a pong within the
pingTimeout
period, the server considers the connection closed. - Similarly, if the client does not receive a ping from the server within the
pingInterval
+pingTimeout
period, the client also considers the connection closed.
We found that in higher versions of socket.io, the server initiates the ping at regular intervals. In contrast, in socket.io 2.X, the built-in heartbeat mechanism is client-initiated. When the browser runs in the background, even if you set a timer to trigger every second, it can only trigger once per minute due to power-saving mechanisms, exceeding the pingInterval
+pingTimeout
settings. Consequently, the logs show a reconnection every minute.
Solution to Frequent WS Disconnections
Upgrade socket.io to the Latest Version
The latest version (4.x) has the server initiating the heartbeat, avoiding the impact of the browser's power-saving mechanism on timers.
Custom WS Heartbeat Events
To minimize the impact on existing business logic, another solution is to use custom heartbeat events. The server sends custom-ping at regular intervals.
Note: Destroy the timer upon disconnection. Although socket.io has a built-in heartbeat (client-initiated in 2.x, server-initiated in 4.x), the custom heartbeat helps maintain data exchange, preventing automatic disconnection and reconnection.
// Client Code
io.on('custom-ping', function () {
io.emit('custom-pong', Date.now())
})
// Server Code
io.on('connection', (socket) => {
console.log('New client connected');
// Send custom ping message
const pingInterval = setInterval(() => {
socket.emit('custom-ping', Date.now());
}, 10000); // Every 10 seconds
// Listen for custom pong message
socket.on('custom-pong', (data) => {
console.log('Pong received:', data);
});
socket.on('disconnect', () => {
clearInterval(pingInterval);
console.log('Client disconnected');
});
});
Note: Destroy the timer upon disconnection.
Although socket.io has a built-in heartbeat (client-initiated in 2.x, server-initiated in 4.x), the custom heartbeat helps maintain data exchange, preventing automatic disconnection and reconnection.
Use setTimeout
Be cautious when using setTimeout
, as it can still lose precision.
// This setTimeout will lose precision
let _cacheTs = Date.now()
const _setTimeoutFn = () => {
console.log('setTimeout :>> ', Date.now() - _cacheTs);
_cacheTs = Date.now()
setTimeout(() => {
_setTimeoutFn()
}, 5000)
}
_setTimeoutFn()
In setTimeout
, executing a function stack is monitored by the browser, similar to setInterval
, and its precision is reduced when running in the background. However, the following approach can avoid the power-saving mechanism's limitations:
// Client Code
// Listen for custom-pong event from the server
socket.on('custom-pong', onHeart)
const onHeart = () => {
if (timer) {
clearTimeout(pingTime.current)
}
timer = window.setTimeout(() => {
socket.emit('custom-ping', Date.now())
}, 5000)
}
// Server Code
socket.on('custom-ping', ()=>{
socket.emit('custom-pong', Date.now())
})
Use Web Workers
Initiating a timer in a Web Worker thread is not affected by the browser's power-saving mechanism.
Conclusion
As browser technologies evolve, power-saving mechanisms will become more refined, posing new challenges for front-end development. Understanding and adapting to these changes, and employing the right strategies to address related issues, is crucial for developing high-quality front-end applications. The methods outlined above can effectively mitigate or resolve the impact of reduced timer precision caused by the browser's power-saving mechanism, thereby enhancing the user experience.
Reference: https://juejin.cn/post/7362576319928008755