likes
comments
collection
share

前端性能基础篇:js时间切分api比较

作者站长头像
站长
· 阅读数 21

1.前置

在开发时,为了提高页面的性能和流畅性,我们不得不关注js执行task的时间问题。从获取时间相关参数到拆分task来提高页面的流畅性涉及到很多的api,本文以此来做相关的讨论。

1.1 console.log

  1. console.log也就是process.stdout.write的执行时间也是要考虑的问题。
  2. 为了使测试结果更准确,不要在执行过程中打印,使用数组存储过程中的记录,最后把数组打印出来。

1.2 统计时间

用performance.now()而不用Date.now():

  1. performance.now()返回当前页面的停留时间,Date.now()返回当前系统时间。但不同的是performance.now()精度更高,且比Date.now()更可靠。
  2. performance.now()返回的是微秒级的,Date.now()只是毫秒级。
  3. performance.now()一个恒定的速率慢慢增加的,它不会受到系统时间的影响。Date.now()受到系统时间影响,系统时间修改Date.now()也会改变。

1.3 为什么要拆分task

我们的浏览器一个页面是单线程的,这就导致浏览器在同一时间只能做一件事,比如:执行js task、用户交互响应、回流重绘等。 而如果一个js task执行时间过长,就会直接导致主线程无法做其他事情,比如反馈用户响应,过度效果的更新等,这就直接导致页面卡顿和丢帧。 下面我摘录部分谷歌官网文章的内容: 为防止主线程被阻塞的时间过长,您可以将一个长任务拆分为几个较小的任务。 前端性能基础篇:js时间切分api比较 这一点很重要,因为当任务分解时,浏览器可以更快地响应优先级更高的工作,包括用户互动。之后,剩余任务会运行完成,确保您最初加入队列的工作已经完成。 前端性能基础篇:js时间切分api比较 在上图的顶部,由用户互动排入队列的事件处理程序必须等待一个长任务才能开始,这就延迟了互动的发生。在这种情况下,用户可能已注意到延迟。在底部,事件处理脚本可以更早开始运行,用户可能感觉到即时互动。

任何耗时超过 50 毫秒的任务都属于耗时较长的任务。对于超过 50 毫秒的任务,其总时间减去 50 毫秒后,称为任务的阻塞期。

1.4 task分割为什么不是微任务

微任务无法真正达到交还主线程控制权的要求。 因为一轮事件循环,是先执行一个宏任务,然后再清空微任务队列里面的任务,如果在清空微任务队列的过程中,依然有新任务插入到微任务队列中的话,还是把这些任务执行完毕才会释放主线程。所以微任务不合适。

2. 拆分task 的api

既然浏览器可以自由调度的最小task是宏任务,那我们只需要将同步执行的代码使用宏任务拆分即可: 我们书写一个 yieldToMain 方法,其内部是使用setTimeout异步api来做到task拆分的作用,利用await/async 将此方法await 之后的代码推入到下一个事件循环中去。

function yieldToMain () {
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

while (tasks.length > 0) {
    if (navigator.scheduling.isInputPending()) {
      await yieldToMain();
    } else {
      const task = tasks.shift();
      task();
    }
  }
}

那我的异步宏任务api有

  • setImmediate
  • MessageChannel
  • requestAnimationFrame
  • requestIdleCallback
  • setInterval
  • setTimeout
  • ...

以下我就逐个对各个api进行详细的讲解:

2.1 MessageChannel

MessageChannel 允许我们建立一个消息通道,并通过两端的端口发送消息实现通信。

深拷贝:其在数据发送的时候会对数据进行深拷贝,相较于JSON.stringify其可以解决undefined和循环引用的问题,但是无法拷贝functionSymbol类型

不过其本身是一个宏任务,所以可以用来做task时间切分。

window.msgList = []
setTimeout(() => {
  window.msgList.push('setTimeout1')
}, 0)
const { port1, port2 } = new MessageChannel()
port2.onmessage = e => {
  window.msgList.push(e.data)
}
port1.postMessage('MessageChannel')

setTimeout(() => {
  window.msgList.push('setTimeout2')
}, 0)

// window.msgList ['setTimeout1', 'MessageChannel', 'setTimeout2']

其执行时机和setTimeout相当,与被调用的先后有关。当然在setTimeout嵌套时其执行肯定会比setTimeout靠前,后文会讲。

2.2 requestAnimationFrame

2.3 requestIdleCallback

定位于执行后台和低优先级任务、执行频率。 在完成一帧中的输入处理、渲染和合成之后,线程会进入空闲时期(idle period),直到下一帧开始,或者队列中的任务被激活,又或者收到了用户新的输入。 requestIdleCallback 定义的回调就是在这段空闲时期执行。(Frame 渲染帧) 前端性能基础篇:js时间切分api比较 如果不存在屏幕刷新,浏览器会安排连续的长度为 50ms 的空闲时期

前端性能基础篇:js时间切分api比较

requestIdleCallback的执行时机是在浏览器重排重绘之后,也就是浏览器的空闲时间执行。其实执行的时机是不准确的,requestIdleCallback执行的JS代码耗时可能会过长。 避免在空闲回调中改变 DOM。 空闲回调执行的时候,当前帧已经结束绘制了,所有布局的更新和计算也已经完成。如果你做的改变影响了布局,你可能会强制停止浏览器并重新计算,而从另一方面来看,这是不必要的。如果你的回调需要改变 DOM,它应该使用Window\.requestAnimationFrame()来调度它。

2.4 setInterval(重点)

setInterval的时间由timer线程统一调度,所以其相应的func被推入事件队列的时间是很准确的, 但是同一个定时器在事件队列里吗等待被执行的方法只能存在一个,也就是如果上一个没被主线程取出执行的话,此次记时结束则不会推入func进入事件队列。 当然倒计时func其被主线程取出并执行的时间是不确定的,并且主线程取出func并执行的同时,timer线程的定时器依然在执行,这就出现了:

  • “丢帧”现象
  • 不同定时器的代码的执行间隔比预期小

我们先来看下面这个例子:

前端性能基础篇:js时间切分api比较

  1. click事件点击
  2. 0.2s 设置定时器 时间间隔为500ms
  3. 0.7s timer线程将定时器回调方法func1推入事件队列,等待主线程执行。
  4. 1s click事件执行结束,浏览器从事件队列中取出func1执行
  5. 1.2s timer线程将func2 回调方法推入事件触发线程,等待主线程执行。
  6. 1.7s timer线程想推入第三次回调方法,但是事件队列中含有该定时器的回调,所以跳过。(丢帧)
  7. 2s func1 执行完毕,在下一轮事件循环立即执行func2方法。

丢帧: 当使用setInterval时,仅当事件队列中没有该定时器的任何其他代码实例时,才将定时器代码添加到事件队列中。

间隔:

  • 时间间隔不受回调函数影响,是由timer线程调度的
  • fun是在每一个周期内被执行
  • func 函数的实际调用间隔要比代码中设定的时间间隔要短

也可能出现这种情况,就是 func 的执行所花费的时间比我们间隔的时间更长。 在这种情况下,JavaScript 引擎会等待 func 执行完成,然后检查调度程序,如果时间到了,则 立即 再次执行它。 极端情况下,如果函数每次执行时间都超过 delay 设置的时间,那么每次调用之间将完全没有停顿。

2.5 setImmediate

只在少量环境(比如 IE 的低版本、Node.js)可以使用

回调将在当前事件循环中的任何I/O操作之后以及为下一个事件循环安排的任何计时器之前执行。 所以setInmmediate应该在setTimeout之前执行。当然文档也说了在没有I/O的情况下,执行顺序是不确定的。 对于react 的调度器其:会优先使用 setImmediate,但它只在少量环境中存在。

2.6 setTimeout(重点)

setTimeout的递归层级过深的话,延迟就不是1ms,而是4ms,这样会造成延迟时间过长,但是我们可以采用修正时间的方法来改变每一次timeout倒计时的时间,比如如下一个60s倒计时:

let time = 60
let passNow = performance.now()
function timeDown(time = 1000){
  setTimeout(() => {
    time--
    if(!time) return
    const currNow = performance.now()
    timeDown(2000 - (currNow - passNow))
    passNow = currNow
  }, time)
}
timeDown()   

2.6.1 浏览器

HTML5标准:如果嵌套的层级超过了 5 层,并且 timeout 小于 4ms,则设置 timeout 为 4ms。 chrome 中的 setTimeout 的行为基本和 HTML5 的标准一致。前 4 次,用的 timeout 都是 1ms以内延迟,后面的间隔时间都超过了 4ms;

2.6.2 nodejs

nodejs 中并没有最小延时 4ms 的限制,而是每次调用都会有 1ms 左右的延时(有时会是0.几毫秒,有时会是1.多毫秒)。

2.6.3 setTimeout和 setImmediate的比较

node v16.18.0 环境下

➜  js-api node  node.js
0.676934003829956 setTimeout
6.1175490617752075 setImmediate
➜  js-api node  node.js
0.7656099796295166 setTimeout
7.105100989341736 setImmediate
➜  js-api node  node.js
0.6082860231399536 setImmediate
5.74574601650238 setTimeout

两个人谁快表现出了一定的随机性,而且setTimeout的延迟也不一定都是1ms,有时会是小于1ms。 但是在I/O回调内,满足永远setImmediate在前

var fs = require('fs')

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('setTimeout', performance.now() - start);
  }, 0);
  setImmediate(() => {
    console.log('setImmediate', performance.now() - start);
  });
});
➜  js-api node  node.js
setImmediate 2.0537240505218506
setTimeout 7.27512800693512
➜  js-api node  node.js
setImmediate 2.034590005874634
setTimeout 7.136919021606445
➜  js-api node  node.js
setImmediate 1.9470280408859253
setTimeout 6.935880064964294
➜  js-api node  node.js
setImmediate 2.1759870052337646
setTimeout 7.4798970222473145
➜  js-api node  node.js
setImmediate 2.302621006965637
setTimeout 7.8137500286102295

参考资料

谷歌官方文档 web.dev/articles/op… node 官网 cnodejs.org/topic/519b5… CSDN-settimeout在各个浏览器的最小时间 blog.csdn.net/weixin_4473… 腾讯云开发者社区 cloud.tencent.com/developer/a… 腾讯云开发者社区-「Nodejs进阶」一文吃透异步I/O和事件循环 cloud.tencent.com/developer/a… 腾讯云开发者社区-setTimeout和setImmediate到底谁先执行,本文让你彻底理解Event Loop cloud.tencent.com/developer/a… 知乎-setImmediate 和setTimeout() 运行顺序?www.zhihu.com/question/56… 你真的了解 setTimeout 么?聊聊 setTimeout 的最小延时问题(附源码细节)www.wangyulue.com/2023/03/%E4… React 的调度系统 Scheduler www.51cto.com/article/741… 对于“不用setInterval,用setTimeout”的理解 segmentfault.com/a/119000001…

转载自:https://juejin.cn/post/7393191747931914290
评论
请登录