Chrome88定时器问题

658 阅读4分钟

背景:刚开工第一天,正值正月初七,公司的客服相应出现socket链接断掉被自动退出登录的问题,刚开始还以为是网络问题,后来开始大面积出现,于是开始了找bug之旅。

刚开工,没发布过代码,而且正好第二天就出现了,于是就开始调研出问题的客服,因为没发布过代码,开始调查发生问题的人,他们的chrome版本都是88,看了下chrome更新日志,正好发布过一个版本,查看它的更新日志 发现有一段更新

怀疑对象来了,简单的说就是为了节省cpu和省电,chrome会对于处在非当前tab的定时器采用每分钟才跑一次的处理,先验证这个是否是真的。

开启一个定时器,每秒跑一次,开启多个浏览器tab,让开启定时器的tab不在当前页面,等待一段时间,得到结果如下

居然有这么大的间隔误差,而且跟说好的每一分钟跑一次也不一致啊,chrome在演我?也可能是我挂机的时间不够长

原因找到了之后,再仔细排查出现的问题,可以将问题分成两个:

  1. 定时器惰性执行导致心跳包发送不及时
  2. 对于在线时长计算的定时器出现计算误差

定时器惰性执行导致心跳包发送不及时

因为chrome过长时间没有发送心跳包,所以导致服务器认为客户端链接已经无效,直接进行断开处理,对于问题1,目前想到有几种解决的方案

  1. 监听visibilitychange事件,切换tab的时候,会有一个hidden的属性,可以在hidden的时候,给服务端发送一个通知,告知其这是暂时离开,可以暂时不用关闭连接
  2. 心跳包由后端发送,前端接收到这个事件给予反馈证明连接还需要保持,这样就没有定时器的问题了
  3. 利用webworker处理,待会儿详细说说这个做法
  4. 后端加大心跳包间隔时间
  5. chrome://flags/#intensive-wake-up-throttling,将其disabled掉

个人认为第二种处理方式是最妥当的,因为chrome限制的是定时器的执行,但服务器不会有这种骚操作,但由于服务端改动起来不太好,所以又想到用webworker去处理了。

WebWorker是HTML5新出的一种方案,其用途是帮助js解决耗时任务卡住js线程的问题。印象中看过一篇文章是说webworker是运行在后台的,所以猜测定时器可能不会受影响。后来直接开启验证,贴上worker代码

// worker.js
let preTime = Date.now();

let timer = setInterval(() => {
  let now = Date.now();

  const timeDiff = Math.floor((now - preTime) / 1000);

  preTime = now;

  self.postMessage({
    type: 'heart-beat'
  });
}, 10 * 1000);

self.addEventListener('message', () => {
  const { type } = e.data || {};
  // 主线程通知关闭当前线程
  if (type === 'interval-thread-close') {
    clearInterval(timer);
    self.close();
  }
});

// main.js
const worker = new Worker('worker.js');

worker.onmessage = (e) => {
  if (e.data.type === 'soft-phone-heart-beat') {
    // console.log('发送心跳包');
    this.doSend('heartBeat');
  }
};

经验证,确实不会受影响,这也太强了。

webworker还有几个点需要注意

  • worker文件跟页面必须得同域
  • 不能操作dom
  • 与主线程不在同一个上下文

问题1解决。。。

-------------------------- 分割线 -------------------------

对于在线时长计算的定时器出现计算误差

这个问题网上也有解决方案,就是记录住上一次进入定时器的时间戳,当下一次进来的时候,用当前时间戳减去上一次时间戳,得到时间差,这个就是时间间隔了,代码如下:

let preTime = Date.now();

const timer = setInterval(() => {
 	const now = Date.now();
    // 算出时间差
    const timeDiff = Math.floor((now - preTime) / 1000);
	// 由于定时器不一定是1000ms一次,有时候992,996都有,所以这里的数有可能小于1
    let count = timeDiff < 1 ? 1 : timeDiff;

    preTime = now;
}, 1000);

问题2也解决了。。。

但还有些谜团没解开

  1. 相同浏览器版本的人,相同操作系统的人,为什么部分人会,部分人不会(猜测是chrome的灰度机制,之前就被第三方cookie问题坑过)
  2. 降级到87版本,然后再升回去,为什么之前出问题的人突然又正常了
  3. 说好的1分钟运行一次定时器,为什么我的mac间隔时间不定的,有8s,10s,15s
  4. 按照chrome://flags/#intensive-wake-up-throttling enabled,发现定时器还是依然准确计时

chrome真的是谜。。。