网站首页 > 技术文章 正文
大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!
setTimeout 通常用于将任务分成更小的部分,以便在执行任务时用户能与页面正常交互,同时在一定的时间后还能继续之前的工作。
然而,不幸的是,大多数浏览器中的 setTimeout 并非真正的 0 毫秒延迟。因此,从这个时间来看,任务并没有尽快完成! 虽然,Chrome 已将其更改为 2 毫秒,但依然存在一些问题。
1.一起看看 setTimeout(0)延迟代码
下面的代码演示了 setTimeout 延迟,即以 0 延迟递归调用 setTimeout 100 次。
正如亲眼所见,运行 100 次迭代的执行时间约为 500 毫秒,您也可以重新执行该代码片段
function bar(iterations) {
if (iterations === 0) {
console.log('done in: ' + (new Date() - startTimeout) + ' msec');
} else {
setTimeout(bar, 0, iterations - 1);
}
}
startTimeout = new Date();
console.log('Start1');
bar(100);
2.为什么 setTimeout 时钟不准
在现代浏览器中,setTimeout 被限制在 4ms 左右,因此协程之间的通信以及挂起、恢复的进度将非常缓慢,因为协程恢复当前是使用零内部 setTimeout 进行调度的。
有多种原因导致 setTimeout 执行可能需要比预期更长的时间。
2.1 嵌套超时
根据 HTML 标准中的规定,一旦对 setTimeout 的嵌套调用被安排了 5 次,浏览器将强制执行 4 毫秒的最小超时。
下面的示例嵌套了对 setTimeout 的调用,延迟为 0 毫秒,并在每次调用处理程序时记录延迟。 前四次延迟约为 0 毫秒,之后约为 4 毫秒:
<button id="run">Run</button>
<table>
<thead>
<tr>
<th>Previous</th>
<th>This</th>
<th>Actual delay</th>
</tr>
</thead>
<tbody id="log"></tbody>
</table>
下面是 JavaScript 代码:
let last = 0;
let iterations = 10;
function timeout() {
// 打印本地调用时间
logline(new Date().getMilliseconds());
if (iterations-- > 0) {
setTimeout(timeout, 0);
}
}
function run() {
const log = document.querySelector('#log');
while (log.lastElementChild) {
log.removeChild(log.lastElementChild);
}
iterations = 10;
last = new Date().getMilliseconds();
setTimeout(timeout, 0);
}
function logline(now) {
const tableBody = document.getElementById('log');
const logRow = tableBody.insertRow();
logRow.insertCell().textContent = last;
logRow.insertCell().textContent = now;
logRow.insertCell().textContent = now - last;
last = now;
}
document.querySelector('#run').addEventListener('click', run);
运行以上代码多次,真实延迟数据如下:
2.2 非活动 Tab 超时
为了减少后台选项卡的负载(以及相关的电池使用量),浏览器将在非活动选项卡中强制执行最小超时延迟。 如果页面使用 Web Audio API AudioContext 播放声音,也可能会被放弃。
具体细节取决于浏览器:
- Firefox Desktop 和 Chrome 的非活动选项卡的最小超时时间均为 1 秒。
- Android 版 Firefox 对于非活动选项卡的最短超时时间为 15 分钟,并且可能会完全卸载。
- 如果选项卡包含 AudioContext,则 Firefox 不会限制非活动选项卡。
2.3 跟踪脚本的限制
Firefox 对识别为跟踪脚本的脚本实施额外的限制。 前台运行时,节流最小延迟仍为 4ms。 然而,在后台选项卡中,限制最小延迟为 10,000 毫秒,即 10 秒,在文档首次加载后 30 秒生效。
2.4 延迟的 setTimeout
如果页面(或操作系统/浏览器)正忙于其他任务,则 setTimeout 也可能比预期晚触发。 需要注意的一个点是,在调用 setTimeout() 的线程终止之前,setTimeout 函数或代码片段无法执行。 例如:
function foo() {
console.log('foo has been called');
}
setTimeout(foo, 0);
console.log('After setTimeout');
控制台将打印:
After setTimeout
foo has been called
这是因为即使 setTimeout 的延迟为零,也会被放置在队列中并计划在下一个循环运行而不是立即。 当前正在执行的代码必须在队列上的函数执行之前完成,因此结果执行顺序可能不符合预期。
2.5 页面加载期间延迟超时
Firefox 将在当前选项卡加载时推迟触发 setTimeout() 计时器。 触发会被推迟,直到主线程被视为空闲(类似于
window.requestIdleCallback()),或者直到触发 load 事件。
2.6 WebExtension 后台页面和计时器
在 WebExtensions 中,setTimeout() 无法可靠地工作。 扩展作者应该使用警报 API。
2.7 最大延迟值
浏览器在内部将延迟存储为 32 位有符号整数。 当使用大于 2,147,483,647 毫秒(约 24.8 天)的延迟时,会导致整数溢出,导致超时立即执行。
3.使用 postMessage 无限接近真正零延迟
下面的方法使用 postMessage 获得相当于 setTimeout(0) 的效果,实现真正的零延迟。
// 将setZeroTimeout添加到window上,其他通过闭包抹平副作用
(function () {
var timeouts = [];
var messageName = 'zero-timeout-message';
// 与setTimeout类似,但是仅仅接受函数参数,不支持时间参数(总是为0)和其他参数(否则需要通过闭包)
function setZeroTimeout(fn) {
timeouts.push(fn);
window.postMessage(messageName, '*');
}
function handleMessage(event) {
if (event.source == window && event.data == messageName) {
event.stopPropagation();
if (timeouts.length > 0) {
var fn = timeouts.shift();
fn();
}
}
}
window.addEventListener('message', handleMessage, true);
// 添加事件监听器到Window对象
window.setZeroTimeout = setZeroTimeout;
})();
实际运行场景,setZeroTimeout 证明比 setTimeout(0) 快得多。 在 Firefox nightly 上,setZeroTimeout 的 100 次迭代的大部分约需要 10-20 毫秒,但有时会更长; 在 WebKit 构建上,需要大约 4-6 毫秒,但有时也会更长一些。
相比之下,在 Firefox 和非基于 Chromium 的 WebKit 上,setTimeout 版本大约需要一秒钟,而在 Windows 上可能更长。
以下是完整的测试代码:
(function () {
var timeouts = [];
var messageName = 'zero-timeout-message';
function setZeroTimeout(fn) {
timeouts.push(fn);
window.postMessage(messageName, '*');
}
function handleMessage(event) {
if (event.source == window && event.data == messageName) {
event.stopPropagation();
if (timeouts.length > 0) {
var fn = timeouts.shift();
fn();
}
}
}
window.addEventListener('message', handleMessage, true);
window.setZeroTimeout = setZeroTimeout;
})();
// 下面使用setZeroTimeout方法
function runtest() {
var output = document.getElementById('output');
var outputText = document.createTextNode('');
output.appendChild(outputText);
function printOutput(line) {
outputText.data += line + '\n';
}
var i = 0;
var startTime = Date.now();
function test1() {
if (++i == 100) {
var endTime = Date.now();
printOutput(
'100 iterations of setZeroTimeout took ' +
(endTime - startTime) +
' milliseconds.'
);
i = 0;
startTime = Date.now();
setTimeout(test2, 0);
} else {
setZeroTimeout(test1);
}
}
setZeroTimeout(test1);
//执行100次,即100次setTimeout和100次setZeroTimeout
function test2() {
if (++i == 100) {
var endTime = Date.now();
printOutput(
'100 iterations of setTimeout(0) took ' +
(endTime - startTime) +
' milliseconds.'
);
} else {
setTimeout(test2, 0);
}
}
}
参考资料
https://dbaron.org/log/20100309-faster-timeouts
https://github.com/Kotlin/kotlinx.coroutines/issues/194
https://bugs.chromium.org/p/chromium/issues/detail?id=888
https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#reasons_for_delays_longer_than_specified
https://blog.klipse.tech/javascript/2016/11/01/setTimeout-0msec.html
https://stackoverflow.com/questions/779379/why-is-settimeoutfn-0-sometimes-useful
https://dmitripavlutin.com/javascript-promises-settimeout/
https://www.educba.com/javascript-settimeout/
猜你喜欢
- 2025-05-15 宇宙厂:深入聊聊 CJS 和 ESM 模块化三点核心差异?
- 2025-05-15 #前端高手进阶#一起薅羊毛~
- 2025-05-15 前端基础进阶(十):深入详解函数的柯里化
- 2025-05-15 2025 年 Object 和 Map 如何选择?
- 2025-05-15 为什么高手写 JS 总是又快又好?这10个技巧你要知道
- 2025-05-15 2025 年 Deno 终于官宣 pnpm 和 Yarn 可使用 JSR?
- 2025-05-15 宇宙厂:为什么前端要了解 Interaction to Next Paint (INP)
- 2025-05-15 Node.js 原生支持 TypeScript?开发者需要了解的一切
- 2025-05-15 请务必用 postTask/isInputPending 释放JS主线程!
- 2025-05-15 未来 JS 标准中的 Map.getOrInsert:彻底告别 if-else 判断?
你 发表评论:
欢迎- 最近发表
-
- 宇宙厂:深入聊聊 CJS 和 ESM 模块化三点核心差异?
- #前端高手进阶#一起薅羊毛~
- 前端基础进阶(十):深入详解函数的柯里化
- 2025 年 Object 和 Map 如何选择?
- 为何说 postMessage 才是真正的 setTimeout(0)?
- 为什么高手写 JS 总是又快又好?这10个技巧你要知道
- 2025 年 Deno 终于官宣 pnpm 和 Yarn 可使用 JSR?
- 宇宙厂:为什么前端要了解 Interaction to Next Paint (INP)
- Node.js 原生支持 TypeScript?开发者需要了解的一切
- 请务必用 postTask/isInputPending 释放JS主线程!
- 标签列表
-
- 前端设计模式 (75)
- 前端性能优化 (51)
- 前端模板 (66)
- 前端跨域 (52)
- 前端缓存 (63)
- 前端react (48)
- 前端md5加密 (49)
- 前端路由 (55)
- 前端数组 (65)
- 前端定时器 (47)
- 前端接口 (46)
- Oracle RAC (73)
- oracle恢复 (76)
- oracle 删除表 (48)
- oracle 用户名 (74)
- oracle 工具 (55)
- oracle 内存 (50)
- oracle 导出表 (57)
- oracle约束 (46)
- oracle 中文 (51)
- oracle链接 (47)
- oracle的函数 (57)
- mac oracle (47)
- 前端调试 (52)
- 前端登录页面 (48)
本文暂时没有评论,来添加一个吧(●'◡'●)