浏览器渲染帧
主流的PC屏幕刷新率(FPS)大多在60Hz,即1秒钟对屏幕进行60次刷新,平均每次刷新耗时大概是16.6ms。
- 刷新率高于60帧/s,会做一些无用的刷新,浪费cpu资源;
- 刷新率低于60帧/s(即用户更新页面,页面内容并未及时修改),会出现丢帧导致页面卡顿,用户体验很差。 那么,在浏览器的进行一次屏幕刷新过程中需要做哪些事情呢?我们借助一张浏览器渲染帧流程图来说明
- 新的一帧开始,首先处理用户输入事件,触发相关event的回调(包括但不限于:touch event、input event、wheel event、click event);
- 查找定时器Timer任务列表,如果定时任务时间到了,执行定时任务;
- 执行上一次渲染帧中注册的 requestAnimationFrame 回调(本次渲染帧注册的requestAnimationFrame 回调在下一次渲染帧才会被执行)
- Parse HTML,如果有DOM变动,那么会有解析DOM的这一过程
- Recalc Styles,如果你在JS执行过程中修改了样式或者改动了DOM,那么便会执行这一步,重新计算指定元素及其子元素的样式
- Layout,如果有涉及元素位置信息的DOM改动或者样式改动,那么浏览器会重新计算所有元素的位置、尺寸信息。而单纯修改color、background等等则不会触发重排
- Update layer tree,更新Render Layer的层叠排序关系
- Paint,计算得出更新图层的绘制指令
- Composite,把绘制指令传到合成线程
- 此时如果主线程(Main Thread)在下一帧到来之前还有时间的话,会执行requestIdleCallback回调
- 合成线程会安排栅格化操作并通知GPU进程刷新这一帧
由此可见,一次屏幕自动刷新流程和页面渲染也差不多
requestAnimationFrame
上面我们提到过requestAnimationFrame,这个api向浏览器注册回调函数,并在每次重排和重绘之前执行该回调,一般用于优化页面性能
const element = document.getElementById('some-element-you-want-to-animate');
let start;
function step(timestamp) {
if (start === undefined)
start = timestamp;
const elapsed = timestamp - start;
//这里使用`Math.min()`确保元素刚好停在200px的位置。
element.style.transform = 'translateX(' + Math.min(0.1 * elapsed, 200) + 'px)';
if (elapsed < 2000) { // 在两秒后停止动画
window.requestAnimationFrame(step);
}
}
window.requestAnimationFrame(step);
注意:若你想在浏览器下次重绘之前继续更新下一帧动画,那么回调函数自身必须再次调用window.requestAnimationFrame()
requestIdleCallback
通过上面的frame执行周期我们可以看到,当浏览器在一个渲染帧(刷新周期)内完成页面渲染操作后,还有剩余时间,会执行 requestIdleCallback 注册的优先级较低的代码。如:
const sleep = (delay) => {
const start = Date.now()
while (Date.now() - start < delay) {}
}
const tasks = [
() => {
console.log('第一个任务开始')
sleep(15)
console.log('第一个任务结束')
},
() => {
console.log('第二个任务开始')
sleep(5)
console.log('第二个任务结束')
},
() => {
console.log('第三个任务开始')
sleep(20)
console.log('第三个任务结束')
},
() => {
console.log('第四个任务开始')
sleep(10)
console.log('第四个任务结束')
},
]
const start = Date.now()
const worker = (deadline) => {
while (
(deadline.timeRemaining() > 0 || deadline.didTimeout) &&
tasks.length > 0
) {
tasks.shift()()
console.log(Date.now() - start)
if (deadline.timeRemaining()) {
console.log('继续执行')
} else {
console.log('时间不够了,下次执行')
}
}
if (tasks.length) {
window.requestIdleCallback(worker, { timeout: 1000 })
}
}
window.requestIdleCallback(worker, { timeout: 1000 })
- sleep是一个简单的睡眠函数,用来模拟超时任务
- tasks是我们需要执行的一个任务列表,包含4个任务;
- worker是我们要执行的低优先级代码;
最后我们调用 requestIdleCallback 注册低优先级代码,在渲染帧内(16.6ms)主线程空闲时去执行该代码块。requestIdleCallback接收两个参数:
- callback:注册的低优先级callback,该函数的入参是一个deadline对象,包含一个timeRemaining函数用来判断当前渲染帧内是否还有空闲时间;didTimeout判断定时器时间是否到了
- options:通常我们需要给低优先级代码指定一个定时器,表示如果定时器时间到了,不管主线程是否空闲,都要强制执行,如{ timeout: 1000 }
上面代码依次输入:
总结
- requestAnimationFrame 和 requestIdleCallback 分别在页面重排重绘一前一后执行;
- requestAnimationFrame在每次重绘前一定会执行,requestIdleCallback并不是一定会执行