Skip to content

定时器

setTimeout 和 setInterval 是 JavaScript 中用于延迟执行代码的两种定时器。

JavaScript 定时器在后台运行的问题

问题现象

javascript
// 场景:用户设置了一个 10 秒的定时器,然后切换到其他标签页

console.log("开始时间:", new Date().toLocaleTimeString());

setTimeout(() => {
  console.log("结束时间:", new Date().toLocaleTimeString());
  console.log("应该 10 秒后执行");
}, 10000);

// 切换标签页后...
// 预期:10 秒后执行
// 实际:可能 30 秒、1 分钟甚至更久才执行

影响范围

  • ✅ setTimeout
  • ✅ setInterval
  • ✅ requestAnimationFrame(完全暂停)
  • ❌ Promise(不直接受影响,但依赖定时器的会受影响)
  • ❌ Web Worker(影响较小)

核心原因

浏览器节流机制(Throttling)

浏览器为了优化性能和节省资源,会对后台标签页进行限制:

前台标签页:定时器正常执行(最小间隔 4ms)

切换到后台:触发节流机制

后台标签页:定时器被限制(最小间隔 1000ms)

设计目的

目的说明
🔋 节省电量减少后台标签页的 CPU 使用
⚡ 提升性能将资源优先分配给活跃标签页
🌡️ 降低发热减少不必要的计算
🛡️ 安全防护防止恶意网站在后台挖矿、攻击等

浏览器策略详解

Chrome/Edge (Chromium 内核)

基础节流规则
javascript
// 前台
setTimeout(() => {}, 0); // 实际最小 ~4ms
setInterval(() => {}, 100); // 正常 100ms

// 后台(页面隐藏 5 分钟内)
setTimeout(() => {}, 0); // 实际最小 1000ms
setInterval(() => {}, 100); // 实际变成 1000ms

// 后台(页面隐藏超过 5 分钟)
// 定时器可能被进一步限制或暂停
渐进式节流策略
页面可见 → 正常执行

隐藏 < 5 分钟 → 最小间隔 1000ms

隐藏 > 5 分钟 → 更激进的限制

长时间未激活 → 可能完全暂停

Firefox

javascript
// 配置项(about:config)
dom.min_background_timeout_value = 1000; // 后台最小间隔(ms)

// 可以修改,但不推荐

Safari

Safari 的策略更加激进:

  • 后台标签页的定时器延迟更严重
  • 可能在几分钟后完全暂停 JavaScript 执行
  • 对移动端(iOS)限制更严格

解决方案

方案 1:Web Worker(推荐)

Web Worker 在独立线程运行,受后台节流影响较小。

javascript
// timer-worker.js
let timerId = null;

self.addEventListener("message", (e) => {
  const { action, delay, interval } = e.data;

  if (action === "setTimeout") {
    timerId = setTimeout(() => {
      self.postMessage({
        type: "timeout",
        timestamp: Date.now(),
      });
    }, delay);
  }

  if (action === "setInterval") {
    timerId = setInterval(() => {
      self.postMessage({
        type: "interval",
        timestamp: Date.now(),
      });
    }, interval);
  }

  if (action === "clear") {
    if (timerId) {
      clearTimeout(timerId);
      clearInterval(timerId);
    }
  }
});
javascript
// main.js
class WorkerTimer {
  constructor() {
    this.worker = new Worker("timer-worker.js");
    this.callbacks = new Map();
  }

  setTimeout(callback, delay) {
    const id = Date.now() + Math.random();

    this.callbacks.set(id, callback);

    this.worker.postMessage({
      action: "setTimeout",
      delay: delay,
      id: id,
    });

    this.worker.onmessage = (e) => {
      if (e.data.type === "timeout") {
        const cb = this.callbacks.get(id);
        if (cb) {
          cb();
          this.callbacks.delete(id);
        }
      }
    };

    return id;
  }

  setInterval(callback, interval) {
    const id = Date.now() + Math.random();

    this.callbacks.set(id, callback);

    this.worker.postMessage({
      action: "setInterval",
      interval: interval,
      id: id,
    });

    this.worker.onmessage = (e) => {
      if (e.data.type === "interval") {
        const cb = this.callbacks.get(id);
        if (cb) cb();
      }
    };

    return id;
  }

  clear(id) {
    this.worker.postMessage({ action: "clear" });
    this.callbacks.delete(id);
  }
}

// 使用示例
const workerTimer = new WorkerTimer();

workerTimer.setTimeout(() => {
  console.log("Worker 定时器执行(更准确)");
}, 10000);

方案 2:服务端定时器(推荐)

服务端定时器在服务器端执行,不受后台节流影响。

最佳实践

  • 使用 Web Worker 或服务端定时器替代 setTimeout 和 setInterval
  • 对于需要后台运行并且时间准确性要求较高的场景,不要依赖前端定时器处理
  • 仅仅在用户可视的时候去试用相关定时器处理任务

基于 MIT 许可发布