定时器
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
- 对于需要后台运行并且时间准确性要求较高的场景,不要依赖前端定时器处理
- 仅仅在用户可视的时候去试用相关定时器处理任务