The Pitfall of WebSocket Disconnections Caused by Browser Power-Saving Mechanisms

  sonic0002        2024-06-23 01:19:39       1,945        0    

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

JAVASCRIPT  WEBSOCKET  POWER SAVING  TROUBLESHOOTING 

       

  RELATED


  0 COMMENT


No comment for this article.



  RANDOM FUN

Don't call me Peter again