编程 Web浏览器的定时器问题思考

2024-11-18 22:19:55 +0800 CST views 861

Web浏览器的定时器问题思考

背景

JavaScript 中,setTimeoutsetInterval 是最常用的延迟或定时循环执行函数的方式,通常会传递第二个参数来控制延迟或间隔执行的时间。然而,开发者必须意识到函数的实际执行时间并不总是精确符合预期,在以下几种情况下,定时器可能会偏离预期:

  1. CPU 繁忙:当主线程被长时间占用时,JavaScript 无法按设定的时间延迟函数执行。
  2. 频繁执行的定时器:如果定时器执行频率过高(如第二个参数小于 4ms),浏览器(或者 JavaScript 引擎)会自动限制其执行频率。
  3. 页面处于后台:浏览器为了节省 CPU 资源和电池消耗,会主动降低定时器的执行频率。

总结来说,定时器在简单的前台检测时通常符合预期,但当页面处于后台或系统资源紧张时,定时器的表现可能会偏离预期。

CPU 繁忙

以下是一个简单的示例,展示当主线程繁忙时,定时器的执行延迟。

const t = performance.now();
setTimeout(() => {
  // 期望 100ms 后执行,实际由于 while 占用了主线程,将在 1s 后执行
  console.log(performance.now() - t);
}, 100);

while (performance.now() - t < 1000) {}

打印结果:

这段代码展示了当主线程忙碌时,定时器的延迟情况,实际的执行时间远超设定的 100ms。

最小延迟时长 >= 4ms

在现代浏览器中,由于函数嵌套层次过深,或者之前的定时器回调函数阻塞了主线程,setTimeoutsetInterval 调用的最小间隔会限制在 4ms 以上。

尝试在不同浏览器中分别执行以下两段代码,观察打印的间隔时间。起初的几次(具体时间取决于浏览器)间隔大约为 1ms,随后间隔会增加到 4ms 以上。

let t = performance.now();
setInterval(() => {
  console.log(performance.now() - t);
  t = performance.now();
}, 1);
let t = performance.now();
function loop() {
  console.log(performance.now() - t);
  t = performance.now();
  setTimeout(loop, 1);
}
loop();

结果表明:小于 4ms 的异步循环任务的延迟时间是无法准确保证的。

后台页面的最小延迟 >= 1000ms

为了减少后台标签页的资源消耗和电池消耗,浏览器通常将后台页面中的定时器最小延迟限制为 1000ms。这意味着一些需要持续后台运行的程序,可能会出现延迟或无法按预期执行的问题。

解决方案:

可以通过使用 WebWorker 中的 setInterval 来向主线程发送消息,主线程会立即响应并执行。这个方法可以绕过浏览器对后台页面的限制。不过,应当谨慎使用这种方法,了解浏览器限制的原因,不要随意绕过这些限制。

基于 WebWorker 的后台定时器

虽然每次需要后台定时器时创建一个 WebWorker 并不太方便,但以下是一个极简的工具函数,它可以在后台页面中定时执行任务。提供简单的 API 来启动和终止定时器。

const setup = (): void => {
  let timerId: number;
  let interval: number = 16.6;

  self.onmessage = (e) => {
    if (e.data.event === 'start') {
      self.clearInterval(timerId);
      timerId = self.setInterval(() => {
        self.postMessage({});
      }, interval);
    }

    if (e.data.event === 'stop') {
      self.clearInterval(timerId);
    }
  };
};

const createWorker = (): Worker => {
  const blob = new Blob([`(${setup.toString()})()`]);
  const url = URL.createObjectURL(blob);
  return new Worker(url);
};

const handlerMap = new Map<number, Set<() => void>>();
let runCount = 1;

const worker = createWorker();
worker.onmessage = () => {
  runCount += 1;
  for (const [k, v] of handlerMap.entries()) {
    if (runCount % k === 0) {
      v.forEach(fn => fn());
    }
  }
};

/**
 * 16.6ms 执行一次回调
 * 解决页面后台时,定时器不(或延迟)执行的问题
 */
export const timer16ByWorker = (handler: () => void, time = 1): () => void => {
  const fns = handlerMap.get(time) ?? new Set();
  fns.add(handler);
  handlerMap.set(time, fns);

  if (handlerMap.size === 1 && fns.size === 1) {
    worker.postMessage({ event: 'start' });
  }

  return () => {
    fns.delete(handler);
    if (fns.size === 0) handlerMap.delete(time);
    if (handlerMap.size === 0) {
      runCount = 0;
      worker.postMessage({ event: 'stop' });
    }
  };
};

使用示例

const stopTimer = timer16ByWorker(() => {
  // 如果期望 setTimeout 的效果,只要执行一次,可以在首次执行时调用 stopTimer
  // 不执行 stopTimer 则类似 setInterval
  // stopTimer();
  
  // do something
}, 1); // 间隔 1 * 16.6ms 执行一次回调

// 终止循环任务
// stopTimer();

选择 16.6ms 作为基础间隔时长的原因

  1. 16.6ms 大约是正常情况下浏览器 60FPS 帧间隔时间,适合执行与渲染相关的任务。
  2. 方便计算更长时间的间隔,例如每 5 秒执行一次循环任务:timer16ByWorker(() => {}, 5 * 60)

如果需要其他时间间隔,也可以修改 let interval = 16.6 的值,但应注意不要设置小于 4ms 的值。

推荐文章

解决python “No module named pip”
2024-11-18 11:49:18 +0800 CST
Vue3中如何实现国际化(i18n)?
2024-11-19 06:35:21 +0800 CST
Vue3中如何实现插件?
2024-11-18 04:27:04 +0800 CST
Nginx负载均衡详解
2024-11-17 07:43:48 +0800 CST
Claude:审美炸裂的网页生成工具
2024-11-19 09:38:41 +0800 CST
快速提升Vue3开发者的效率和界面
2025-05-11 23:37:03 +0800 CST
liunx宝塔php7.3安装mongodb扩展
2024-11-17 11:56:14 +0800 CST
25个实用的JavaScript单行代码片段
2024-11-18 04:59:49 +0800 CST
pin.gl是基于WebRTC的屏幕共享工具
2024-11-19 06:38:05 +0800 CST
nuxt.js服务端渲染框架
2024-11-17 18:20:42 +0800 CST
H5端向App端通信(Uniapp 必会)
2025-02-20 10:32:26 +0800 CST
PHP设计模式:单例模式
2024-11-18 18:31:43 +0800 CST
CSS实现亚克力和磨砂玻璃效果
2024-11-18 01:21:20 +0800 CST
Vue中如何使用API发送异步请求?
2024-11-19 10:04:27 +0800 CST
JavaScript 流程控制
2024-11-19 05:14:38 +0800 CST
12 个精选 MCP 网站推荐
2025-06-10 13:26:28 +0800 CST
避免 Go 语言中的接口污染
2024-11-19 05:20:53 +0800 CST
Linux 网站访问日志分析脚本
2024-11-18 19:58:45 +0800 CST
Vue3的虚拟DOM是如何提高性能的?
2024-11-18 22:12:20 +0800 CST
liunx服务器监控workerman进程守护
2024-11-18 13:28:44 +0800 CST
LangChain快速上手
2025-03-09 22:30:10 +0800 CST
38个实用的JavaScript技巧
2024-11-19 07:42:44 +0800 CST
程序员茄子在线接单