文章目录
- 浏览器渲染机制
- 事件循环机制
- 宏队列与微队列
- 浏览器中事件循环流程
- requestAnimationFrame(rAF)
- requestAnimationFrame API
- requestIdleCallback
- requestIdleCallback API
- 任务拆分
- requestIdleCallback的使用场景
浏览器渲染机制
- 每一轮
Event Loop
都会伴随着渲染吗? requestAnimationFrame
在哪个阶段执行,在渲染前还是后?在microTask
的前还是后?
requestAnimationFrame
在重新渲染屏幕之前执行requestIdleCallback
在哪个阶段执行?如何去执行?在渲染前还是后?
requestIdleCallback
在渲染屏幕之后执行,并且是否有空执行要看浏览器的调度
事件循环机制
作用:事件循环机制的作用是协调事件、用户交互、脚本、渲染及网络任务等。
宏队列与微队列
一个事件循环有一个或多个宏队列,有一个微队列
一个宏队列在数据结构上是一个集合(叫做任务队列),事件循环处理模型会从选定的任务队列中获取一个可运行任务。微队列是FIFO先进先出队列。
- 宏任务
- setTimeout、setInterval
- setImmediate(node 独有)
- DOM事件、Ajax事件
- 用户交互、用户操作事件
- script(整体代码)
- indexDB操作
- 微任务
- process.nextTick
- Promise一些方法,如.then
- Async/Await(实际就是promise)
- MutationObserver(html5新特性)
浏览器中事件循环流程
- 同步任务和异步任务进入不同的执行环境,同步任务放入执行栈中,异步任务放入任务队列中。
- 先执行同步代码
- 检查微任务队列,执行并清空微任务队列,如果在微任务的执行中又加入了新的微任务,也会在这一步一起执行。
- 进入更新渲染阶段,判断是否需要渲染(根据屏幕刷新率、页面性能等)。并不是每轮事件循环都会执行浏览器渲染
- 没有就开启下一轮循环,取出一个宏任务执行。一个宏任务执行完毕后就清空微队列,然后见检查需不需要。循环这个过程
DOM的修改不会立刻导致渲染,渲染线程和Javascript线程是互斥的,必须等待Javascript的这次调度执行完或线程挂起了,才能执行渲染。
这次调度可以看成是一轮事件循环完,一次事件循环=宏任务(第一次是同步代码)+微任务
requestAnimationFrame(rAF)
是什么
requestAnimationFrame
是H5新增的API类似于setTimeout
,告诉浏览器在重新渲染屏幕之前执行。主要用途是按帧对网页进行重绘。
rAF是官方推荐的用来做一些流畅动画所应该使用的 API,做动画不可避免的会去更改 DOM,而如果在渲染之后再去更改 DOM,那就只能等到下一轮渲染机会的时候才能去绘制出来了
优势
调用时机:在重新渲染前调用。
requestAnimationFrame
最大的优势是由系统来决定回调函数的执行时机。保证回调函数在屏幕每一次刷新间隔中只执行一次,避免丢帧
如果浏览器不渲染,是不是就不会调用requestAnimationFrame? 如果requestAnimationFrame做太多事情,会导致降频,比如1s刷新60次变成1s刷新30次。
requestAnimationFrame API
基本语法:requestAnimationFrame (callback)
返回值:回调函数列表中的唯一值,可以使用cancelAnimationFrame
传入请求ID取消回调函数。
说明
requestAnimationFrame
不管理回调函数,意思是多次调用带有同一回调函数的requestAnimationFrame
,会导致回调在同一帧中执行多次。
由rAF的返回值是回调函数列表中的唯一值,可以理解为即使是同一回调函数,但是在回调函数列表中的值都是不一样的。
所以配合cancelAnimationFrame
使用。- 当
requestAnimationFrame()
运行在后台标签页或者隐藏的<iframe>
里时,requestAnimationFrame()
会被暂停调用以提升性能和电池寿命。
requestIdleCallback
对于人眼来说,当每秒切换60张图片时,就会认为是连贯的。所以主流的显示器是60hz的(1s刷新60次),那么每16.7ms需要刷新一次,浏览器会自动适配这个频率,这时对应前端页面就是每16.7ms需要渲染一次。
页面每隔16.7ms才会渲染一次,那么在两次渲染的中间时间,就是浏览器的空闲时间,在这段空闲时间执行的任务,是不会阻塞到页面渲染的流畅性的。
如果在某一帧区间内执行过多的任务会导致下一帧一直没办法渲染,页面看起来就被卡住。
对大量任务的计算首先考虑Web Worker 使其不占用主线程,如果需要操作DOM,可以考虑任务拆分。
图中一帧包括了用户的交互, JavaScript 脚本执行; 以及requestAnimationFrame(rAF)
的调用, 布局计算以及页面重绘等。如果某一帧里执行的任务不多, 在不到 16.66ms内就完成了上述任务, 那么这一帧就会有一定空闲时间来执行requestIdleCallback
的回调。会在layout/paint
之前调用。
回流也叫重排(layout),当 DOM 的变化影响了元素的几何信息(位置、尺寸大小等),浏览器需要重新计算元素的几何属性,将其安放在界面的正确位置,这个过程叫做回流。
当一个元素的外观发生变化,但没有改变布局,重新把元素外观绘制出来的过程,叫做重绘(paint)。
requestIdleCallback API
基本语法:requestIdleCallback(callback,options)
- 当
callback
被调用时,回接受一个参数deadline
,deadline
是一个对象,对象上有两个属性timeRemaining
属性是一个函数,函数的返回值表示当前空闲时间还剩下多少时间didTimeout
,didTimeout
属性是一个布尔值,如果didTimeout
是true
,那么表示本次callback
的执行是因为超时的原因
options
是一个对象,可以用来配置超时时间。如果指定了timeout
,但是浏览器没有在timeout
指定的时间内,执行callback
。在下次空闲时间时,callback
会强制执行。并且callback
的参数,deadline.didTimeout=true
,deadline.timeRemaining()
返回0。
空闲时间
在空闲期间,callback的执行顺序是以FIFO(先进先出)的顺序。但是如果在空闲时间内依次执行callback时,有一个callback的执行时间,已经将空闲时间用完了,剩下的callback将会在下一次的空闲时间执行。
const startTask = (deadline) {
// 如果 `task` 花费的时间是20ms
// 超过了当前空闲时间的剩余毫秒数,我们等到下一次空闲时间执行task
if (deadline.timeRemaining() <= 20) {
// 将任务带到下一个空闲时间周期内
// 添加到下一个空闲时间周期callback列表的末尾
requestIdleCallback(startTask)
} else {
// 执行任务
task()
}
}
当网页处于不可见的状态时(比如切换到其他的tag),空闲时间将会每10s, 触发一次空闲期。
任务拆分
将批量的任务进行拆分,保证这些任务只在空闲时间
执行。每次执行下一个任务时,先检查当前页面是否该渲染下一帧了,如果是则让出线程,进行页面渲染。
requestIdleCallback
是浏览器提供给我们用来判断这个时间的api,它会在浏览器空闲的时候来执行其回调函数,如果指定了超时时间,会在超时后的下一帧强制执行。
const id = window.requestIdleCallback((deadline) => {
// 当前帧剩余时间大于0,或任务已超时
if(deadline.timeRemaining() > 0 || deadline.didTimeout) {
// do something
console.log(1)
}
}, { timeout: 2000 }) // 指定超时时间
// window.cancelIdleCallback(id) 与定时器类似,支持取消
requestIdleCallback
在Event Loop的执行时机如下图所示,蓝色区域代表一帧内的渲染任务,当这些任务执行完后,剩余的时间被认为是空闲时间。
使用案例
// class中的一个方法
idleDownload(){
// 先取消之前的
cancelIdleCallback(this.ridId); // ridId是class中的属性,存放一个requestIdleCallback的id
const { tasks } = this // tasks为总任务数
let index = 0; // 任务索引
const ridOption = {timeout:2000}; // 指定超时时间,会在超时后的下一帧强制执行。
// 当前帧空闲时执行的回调函数
const handler = (idleDeadline) => {
const {timeRemaining} = idleDeadline; // 获取空闲时间
while(timeRemaining()>0 && index<tasks.length ){
// 在空闲时间执行任务
index ++;
}
// 判断任务是否下载完成
if(index< tasks.length){ // 不空闲了,但是任务还没有执行完毕
this.ridId = requestIdleCallback(handler, ridOption); // 继续等待下次空闲时下载
}else{
// 已经下载完毕
}
}
this.ridId = requestIdleCallback(handler, ridOption);
}
requestIdleCallback的使用场景
适用场景
- 预加载
- 检测卡顿
如果requestIdleCallback
长时间内没能得到执行,说明一直没有空闲时间,很有可能就是发生了卡顿,从而可以打点上报。它比较适用于行为卡顿,举个例子:点击某个按钮并同时添加requestIdleCallback
回调,如果点击后的一段时间内这个回调没有得到执行,很大概率是这个点击操作造成了卡顿。 - 拆分耗时任务
不适用场景
- 更新DOM操作
requestIdleCallback
回调执行之前, 样式变更以及布局计算等都已经完成。如果在callback
中修改DOM
, 之前所作的布局计算都会失效。 并且如果下一帧里有获取布局相关的操作, 浏览器就需要强制进行重排, 极大的影响性能。 另外由于修改 DOM 的时间是不可预测的, 因此容易超过当前帧空闲的阈值. promise
的回调(resolve/reject
)属于优先级较高任务,在一帧的过程中如果产生了微任务会执行微任务。所以会在 requestIdleCallback 回调结束后立即执行,可能会给这一帧带来超时的风险。
// console
// 空闲时间1
// 等待了1000ms
// 空闲时间2
// Promise 会在空闲时间1接受后立即执行,即使没有空闲时间了也是如此。拖延了进入下一帧的时间
requestIdleCallback(() => {
console.log('空闲时间1')
Promise.resolve().then(() => {
sleep(1000)
console.log('等待了1000ms')
})
})
requestIdleCallback(() => {
console.log('空闲时间2')
})
参考文章:
批量任务导致页面卡死?试试requestIdleCallback对任务进行拆分
详解 requestIdleCallback
requestAnimationFrame 执行机制探索