你踩了吗?浏览器节能机制的坑🕳️🕳️🕳️

序言

近期,在使用WebSocket(WS)连接时遇到了频繁断连的问题,这种情况在单个用户上每天发生数百次。尽管利用了socket.io的自动重连机制能够在断连后迅速恢复连接,但这并不保证每一次重连都能成功接收WS消息。因此,我们进行了一些的排查和测试工作。

最终发现问题的根本原因:正是浏览器的节能机制,不经意间成为了这一问题的幕后黑手

浏览器节能机制简介

浏览器的节能机制逐渐成为前端开发者需要关注的问题。特别是这些节能机制可能会对定时器的精度产生影响,这直接关系到前端应用的用户体验,在某些场景下甚至影响到用户的使用。

为了减少电能消耗,提高电池续航能力,现代浏览器都引入了节能机制。这些机制包括但不限于降低空闲标签页的CPU使用率、减少后台JavaScript的执行频率、限制定时器的精确度等。虽然这些措施显著提高了设备的能效,但也给前端开发带来了一些挑战。

WS频繁断连原因分析

查阅socket.io官网服务端配置的pingTimeoutpingInterval两个参数发现WS心跳异常时会导致重连,具体说明:

WS连接中服务端和客户端两端必须一致保持心跳。如果有一端停止,则满足如下条件之一就会自动断连:

  • 服务器发送 ping,如果客户端在毫秒内 pingTimeout 没有用 pong 应答,则服务器认为连接已关闭。
  • 同样,如果客户端在毫秒内 pingInterval + pingTimeout 未收到来自服务器的 ping,则客户端也会认为连接已关闭。

看文档发现其实高版本的socket.io是由服务端定时发起ping。而在socket.io 2.X的版本中内置的心跳机制是由客服端定时发起。而浏览器在后台运行时,即使你设置了一个每秒触发的定时器,它也只能每分钟触发一次 ,超过了pingInterval + pingTimeout设置的时间,最后看到的日志是很有规律的每分钟重连一次。

WS频繁断连解决方法

升级socket.io到最新版本

上面的截图其实就是最近版本(4.x)的,升级后由服务器定时发起心跳。在服务端运行定时,避开了浏览器节能机制对定时的影响

自定义WS心跳

为了减小直接升级对已有业务的影响,目前使用的也是这种方案:在服务端自定义事件,定时发送心跳custom-ping

javascript 复制代码
// 客服的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就不会自动中断重连。

  1. 页面保活(在后台运行时也保持浏览器的活跃)

用得最多的方式是在页面隐藏一个循环播放的音频 或者 使用nosleep.js

javascript 复制代码
const noSleepInstance = new NoSleep();
document.addEventListener('click', function enableNoSleep() {
  document.removeEventListener('click', enableNoSleep, false);
  noSleepInstance.enable();
}, false);

实测,使用这种方式时,浏览器在后台运行仍然存在定时器精度降低的问题

  1. 使用setTimeout

这里要注意使用setTimeout的姿势,如果是直接这样使用、依然会有精度问题:

javascript 复制代码
let _cacheTs = Date.now()
const _setTimeoutFn = () => {
  console.log('setTimeout :>> ', Date.now() - _cacheTs);
  _cacheTs = Date.now()
  setTimeout(() => {
    _setTimeoutFn()
  }, 5000)
}
_setTimeoutFn()

在setTimeout里面去执行一个函数栈会被浏览器监控到,会认为和setInterval一样,其在后台运行时会降低其定时精度。但如果这样可以避开节能机制的限制:

javascript 复制代码
// 客服端CODE
// 监听服务端发送的custom-pong事件
socket.on('custom-pong', onHeart)

const onHeart = () => {
  if (pingTime.current) {
    clearTimeout(pingTime.current)
  }
  pingTime.current = window.setTimeout(() => {
    socket.emit('custom-ping', {
      pingTime: Date.now(),
      chatroom: lecture.chatroom,
    })
  }, 5000)
}

// 服务端CODE
socket.on('custom-ping', ()=>{
  socket.emit('custom-pong', Date.now())
})

总结

随着浏览器技术的发展,节能机制无疑会越来越完善,但与此同时也给前端开发带来了新的挑战。了解和适应这些变化,采用正确的策略来解决由此引起的问题,对于开发高质量的前端应用至关重要。通过上述方法,我们可以有效地缓解或解决浏览器节能机制对定时器精度降低带来的影响,从而提升用户体验。

相关推荐
mldong4 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排5 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60615 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角6 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!6 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰7 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi8 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen9 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马9 小时前
AI源码分析:拆解模型底层实现逻辑
前端
BillKu10 小时前
全局样式变量:CSS 自定义属性(--border-color)和SCSS 变量($border-color)的说明
javascript·css·scss