网站首页 > 技术文章 正文
大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发,您的支持是我不断创作的动力。
即使使用 JavaScript 很长一段时间,很多开发者也可能会惊叹为何 setTimeout(0) 并不是真正的 setTimeout(0)。相反,其可能会延迟 4 毫秒运行:
// W3C 规范:如果嵌套级别大于 5,并且超时小于 4ms,则将超时设置为 4ms
function createNestedTimeout(nestingLevel) {
if (nestingLevel > 10) return;
// 防止无限
const start = performance.now();
setTimeout(() => {
const delay = performance.now() - start;
console.log(` 嵌套层级 ${nestingLevel}: 实际延迟 ≈ ${delay.toFixed(2)} ms`);
// 在 setTimeout 回调中直接递归调用下一个 setTimeout
// 此时才会被浏览器视为 “嵌套定时器”
createNestedTimeout(nestingLevel + 1);
}, 0);
}
createNestedTimeout(1);
Microsoft Edge 团队开发者给出的解释是:浏览器这样做是为了避免 setTimeout 被滥用。 为了尽量避免耗尽用户电量或阻碍用户交互,浏览器会设置一个特殊的限制 ,即最小值 4ms。
这也从侧面解释了为何有些浏览器会对使用电池供电的设备加大节流(旧版 Edge 为 16ms)或对后台标签页进行更严格的节流(Chrome 为 1000ms)。
然而这又引申出另一个问题:如果 setTimeout 被滥用,那为何浏览器还要不断引入新的计时器,例如:setImmediate、Promises,甚至 scheduler.postTask() 呢?同时如果 setTimeout 必须被削弱,那么这些计时器最终会不会也遭遇同样的命运?
Nolan Lawson 在 2018 年写过一篇 JavaScript 计时器的长文,最近他又开发了 fake-indexeddb 库,该库是 IndexedDB 的纯 JavaScript 实现,于是开始重新审视这个问题。IndexedDB 希望在事件循环中没有未完成的工作时自动提交事务,即在所有微任务完成后但在任何新宏任务开始之前 。
为了实现目标,fake-indexeddb 在 Node.js 中使用了 setImmediate,而在浏览器中使用了 setTimeout。在 Node 中 setImmediate 非常完美,因为其会在所有微任务之后、但在任何其他任务之前立即运行。然而在浏览器中,setTimeout 的表现欠佳: 在一个基准测试中,发现 Chrome 浏览器需要 4.8 秒才能完成 Node 中仅需 300 毫秒的任务,即速度降低了 16 倍!。
同时,展望 2025 年的计时器格局,仍然很难做出明确的选择。
- setImmediate:仅支持旧版 Edge 和 IE,因此无法使用
- MessageChannel.postMessage:MessageChannel 接口允许创建一个新的消息通道并通过两个 MessagePort 属性发送数据
const channel = new MessageChannel();
const output = document.querySelector(".output");
const iframe = document.querySelector("iframe");
// 等待 iframe 加载完成
iframe.addEventListener("load", onLoad);
function onLoad() {
// 监听 port1 的消息
channel.port1.onmessage = onMessage;
// 将 port2 发送给 iframe
iframe.contentWindow.postMessage("Hello from the main page!", "*", [
channel.port2,
]);
}
// 监听 port1 收到的消息
function onMessage(e) {
output.innerHTML = e.data;
}
- window.postMessage :想法不错但有点卡顿,因为其可能会干扰页面上使用相同 API 的其他脚本 (setImmediate 的 polyfill 使用了该方法)
const myWorker = new Worker("worker.js");
if (crossOriginIsolated) {
const buffer = new SharedArrayBuffer(16);
myWorker.postMessage(buffer);
} else {
const buffer = new ArrayBuffer(16);
myWorker.postMessage(buffer);
}
- scheduler.postTask :最佳选择
为了比较以上诸多选项,下面编写了一个基准测试,且基准测试有一些限制:
- 必须多次迭代 setTimeout 才能真正了解限制机制。从技术上讲,根据 HTML 规范,只有在 setTimeout 嵌套 5 次后,4 毫秒的限制才会生效
- 没有测试所有可能的组合:1)电池供电还是插电供电;2)显示器刷新率;3)背景标签页还是前景标签页等等。
const pre = document.querySelector("pre");
const log = (str) => {
pre.textContent += str + "\n";
};
const methods = {
setTimeout: () => {
return new Promise((resolve) =>
setTimeout(() => resolve(performance.measure("setTimeout", "start")))
);
},
messageChannel: () => {
return new Promise((resolve) => {
const channel = new MessageChannel();
channel.port1.onmessage = () =>
resolve(performance.measure("messageChannel", "start"));
channel.port2.postMessage(undefined);
});
},
window: () => {
return new Promise((resolve) => {
window.addEventListener(
"message",
() => resolve(performance.measure("window", "start")),
{ once: true }
);
window.postMessage(undefined);
});
},
...(typeof scheduler !== "undefined" && {
postTask: () => {
return new Promise((resolve) =>
scheduler.postTask(() =>
resolve(performance.measure("postTask", "start"))
)
);
},
}),
};
async function main() {
const medians = [];
for (const [method, func] of Object.entries(methods)) {
const results = [];
for (let i = 0; i < 101; i++) {
performance.mark("start");
// 开始标记
results.push(await func());
// 结束标记
}
const median =
results.map((_) => _.duration).reduce((a, b) => a + b, 0) /
results.length;
log(`${method}: ${median} (median of ${results.length} iterations)`);
medians.push(median);
}
log("\nAs table:");
log(medians.map((median) => median.toFixed(2)).join("|"));
}
main();
以下是一些数值(以毫秒为单位,101 次迭代的中位数,基于 2021 年 16 英寸 MacBook Pro):
开发者不必过于担心确切的数字,重点是 Chrome 和 Firefox 将 setTimeout 限制在 4 毫秒,其他三个选项大致相同。有趣的是,在 Safari 中,setTimeout 受到的限制更为严重,
MessageChannel.postMessage 比 window.postMessage 稍慢一些。
感悟:现代浏览器(尤其是 Chrome 和 Firefox)实际上已经将 setTimeout(0) 的最小延迟默认 clamped 到 4ms,即使在非嵌套场景下。这看似违反 HTML 规范,但实际上是浏览器厂商出于性能、功耗和兼容性考虑所做的主动干预(intervntion)。
因此几乎可以得出结论:fake-indexeddb 应该使用 scheduler.postTask,然后 fallback 到
MessageChannel.postMessage 或 window.postMessage。
然而到此还是没回答先前的疑问:既然 Web 开发者可以直接使用 scheduler.postTask 或 MessageChannel,为什么浏览器还要费心去限制 setTimeout 呢?下面是来自 Web 性能工作组 (Web Performance Working Group)Todd Reifsteck 的回答:
总体观点是开发者实际上存在两个阵营:
- 限制派:需要限制计时器以保护 Web 开发者免受自身缺陷的影响
- 非限制派:开发者应该 “衡量自己的愚蠢程度”,任何微妙的限制启发式方法只会造成混乱
因此大致可以得出结论:浏览器干预通常是因为 Web 开发者要么用得太多(例如 setTimeout),要么就是完全没有意识到更好的选择。归根结底,浏览器是代表用户行事的 “用户代理”,而 W3C 的优先级明确指出,最终用户的需求始终高于 Web 开发者的需求。因此,通过赋予开发者对任务和调度更多的控制权,可以避免反复使用 setTimeout 并造成需要干预的混乱。
同时,postTask/postMessage 可能暂时不会受到限制。在 Todd Reifsteck 提出的两个阵营中,Scheduler API 的存在本身就表明,其提供了一系列用于任务调度的细粒度工具。尽管 Todd Reifsteck 认为该 API 更像是两派之间的妥协:其提供了大量的控制权,但也与浏览器的实际渲染管道(而非随机超时)保持一致。
然而还是需要从悲观的角度思考下该 API 是否仍然可能被滥用,例如:在任何地方不经意地使用 user-blocking 优先级,又或许在未来一些有进取心的浏览器厂商会更加坚定地踩下油门,并发现其能让网站运行得更快、响应更快、更省电。如果这种情况发生,可能会看到新一轮的干预。
参考资料
文章主体内容来自Nolan Lawson发表的文章《Why do browsers throttle JavaScript timers?》,但是对部分内容添加了自己的理解。
https://nolanlawson.com/2025/08/31/why-do-browsers-throttle-javascript-timers/
https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#timer-initialisation-steps
https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout#reasons_for_delays_longer_than_specified
https://dev.to/jeetvora331/throttling-in-javascript-easiest-explanation-1081
猜你喜欢
- 2025-10-08 如何计算浏览器页面的帧数 FPS?_怎么测试浏览器性能
- 2025-10-08 新一代浏览器 Web 引擎 Ladybird 已狂揽 4.4w star!
- 2025-10-08 每个前端开发者应该掌握的10个浏览器技巧
- 2025-01-07 浏览器运行 Java 的7种尖端技术!
- 2025-01-07 网页如何唤起应用程序?
- 2025-01-07 Chatty:如何启动 WebGPU 在浏览器运行 LLM ?
- 2025-01-07 前端跨浏览器标签页数据共享解决方案
- 2025-01-07 vue项目如何有效解决的浏览器的缓存问题
- 2025-01-07 你知道HTML、CSS、JS文件在浏览器中是如何转化成页面的吗?
- 2025-01-07 VS Code如何内置Chrome浏览器?超简单
你 发表评论:
欢迎- 最近发表
-
- Three.js vs Unity:工业可视化为何选择Web方案?
- 一款全新Redis UI可视化管理工具,支持WebUI和桌面——P3X Redis UI
- 时间线可视化实战:三款AI工具实测,手把手教你制作人生轨迹图
- 【推荐】一款可视化在线 Web 定时任务管理平台,支持秒级任务设置
- 重磅更新!FastDatasets 推出可视化 Web 界面
- 模具设计之UG钣金实例教程(3)_ug钣金基础教程
- 前端基于 RBAC 模型的权限管理实现
- 别再把JWT存在localStorage里了!2025年前端鉴权新思路
- 模具设计之曲面造型中不圆润的曲面如何处理技巧
- 9个专业级别的CSS技巧区分了解和精通的鸿沟
- 标签列表
-
- 前端设计模式 (75)
- 前端性能优化 (51)
- 前端模板 (66)
- 前端跨域 (52)
- 前端缓存 (63)
- 前端aes加密 (58)
- 前端脚手架 (56)
- 前端md5加密 (54)
- 前端路由 (61)
- 前端数组 (73)
- 前端js面试题 (50)
- 前端定时器 (59)
- Oracle RAC (76)
- oracle恢复 (77)
- oracle 删除表 (52)
- oracle 用户名 (80)
- oracle 工具 (55)
- oracle 内存 (55)
- oracle 导出表 (62)
- oracle约束 (54)
- oracle 中文 (51)
- oracle链接 (54)
- oracle的函数 (58)
- oracle面试 (55)
- 前端调试 (52)
本文暂时没有评论,来添加一个吧(●'◡'●)