屏幕刷新率
- 目前大多数设备的屏幕刷新率是60次每秒
- 浏览器渲染动画或者页面的每一帧的速率也需要根设备屏幕的刷新率保持一致
- 页面是一帧一帧绘制出来的,当每秒的帧数(FPS)达到60,页面是流畅的,小于这个值,用户会感觉到卡顿
- 每一帧的预算时间大概是16.6ms
- 1s 60帧,所以每帧时间大概16.6ms,我们书写代码时力求不让每一帧工作量超过16.6
每帧浏览器都做了什么
- 阻塞输入事件(输入事件,触摸事件,鼠标滚动事件),非阻塞输入事件(点击,键盘事件)。因为和用户交互,优先级最高
- 定时器,js脚本,微任务
- 开始帧(每一帧的事件,如窗口resize事件,滚动,媒体查询事件)
- requestAnimateFrame
- layout布局,计算样式,更新布局
- 绘制paint,记录,合成图层等
- 空闲阶段(idle peroid)requestIdleCallback
requestAnimationFrame
该api告诉浏览器,希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行
执行时机是每一帧浏览器进行渲染之前,是一个高优的任务。
当你准备更新动画时你应该调用此方法。这将使浏览器在下一次重绘之前调用你传入给该方法的动画函数 (即你的回调函数)。
回调函数执行次数通常是每秒 60 次,但在大多数遵循 W3C 建议的浏览器中,回调函数执行次数通常与浏览器屏幕刷新次数相匹配。为了提高性能和电池寿命,因此在大多数浏览器里,当requestAnimationFrame()
运行在后台标签页或者隐藏的<iframe>
里时,requestAnimationFrame()
会被暂停调用以提升性能和电池寿命。
也就是说,当我们切换浏览器的tab页面,或者浏览器窗口最小化以后,使用改api模拟的动画会暂停,直到当前页面再次出现在屏幕的可视区域。
回调函数会被传入DOMHighResTimeStamp
参数,DOMHighResTimeStamp
指示当前被 requestAnimationFrame()
排序的回调函数被触发的时间。在同一个帧中的多个回调函数,它们每一个都会接受到一个相同的时间戳,即使在计算上一个回调函数的工作负载期间已经消耗了一些时间。该时间戳是一个十进制数,单位毫秒,最小精度为 1ms(1000μs)。
通俗的说,该api会给传递的回调函数一个参数,该参数是当前页面渲染完成到当前函数执行时,已经过去了多少时间。这个参数的值在后续的每次渲染都会不断增加,因为浏览器渲染完毕以后,时间总是在增加。当然呢刷新页面以后会被重置。
demo:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
div {
height: 200px;
width: 0;
background-color: aqua;
transition: all 0.1s;
}
</style>
</head>
<body>
<div></div>
<script>
const div = document . querySelector('div')
let width = 0
const animationWidth = (time) => {
width += 10
div.style.width = width + 'px'
div.innerHTML = (width / 10) + '%'
// 如果页面渲染了两秒时间过去了还没执行完 就不执行动画了 if (width < 1000 && time < 2000) requestAnimationFrame(animationWidth)
}
requestAnimationFrame(animationWidth)
</script>
</body>
</html>
我们在回调函数中,再把当前的动画回调函数放入requestAnimationFrame中,会在下一次渲染页面前继续执行该动画函数
requestIdleCallback
window.requestIdleCallback()
方法插入一个函数,这个函数将在浏览器空闲时期被调用。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。函数一般会按先进先调用的顺序执行,然而,如果回调函数指定了执行超时时间timeout
,则有可能为了在超时前执行函数而打乱执行顺序。
在浏览器每次渲染页面时,我们会反馈(响应用户的输入),页面的拖拽,浏览器大小的改变,js主线程的任务,微任务宏任务,requestAnimationFrame回调,浏览器进行绘制,如果这些事情做完以后,还有剩余时间,我们认为这就是当前渲染帧的空闲时间,这个时间浏览器可以交给我们开发者(js主线程执行一些优先级不高的任务)。
可以这样认为,我们使用这个api了,那么浏览器在当前帧还有剩余时间,那么就会把执行权交给我们,但是我们也需要在剩余时间结束之前把执行权还给浏览器。
要记住,拿到执行权,然后在使用完毕以后,把执行权还给浏览器,这个是我们开发者和浏览器的 “君子协议”,你当然拿到执行权可以不交给浏览器了(比如我们进行一次死循环,那么很显然浏览器会卡死,没办法响应用户输入等)。
demo:
const sleep = (time) => {
const now = Date . now()
while (Date . now() < now + time) { }
}
const worksQueue = [
() => {
console . log('任务A开始')
sleep(20)
console . log('任务A结束')
},
() => {
console . log('任务B开始')
sleep(20)
console . log('任务B结束')
},
() => {
console . log('任务C开始')
sleep(20)
console . log('任务C结束')
},
() => {
console . log('任务D开始')
sleep(20)
console . log('任务D结束')
},
() => {
console . log('任务E开始')
sleep(20)
console . log('任务E结束')
}
]
const doSomething = (idleDeadline) => { // idleDeadline.timeRemaining() // 返回值是剩余时间 while (idleDeadline. timeRemaining() > 0 && worksQueue.length) {
console . log(idleDeadline. timeRemaining())
const work = worksQueue. shift()
work()
}
// 还有任务 if(worksQueue.length) requestIdleCallback(doSomething)
}
requestIdleCallback(doSomething)
看打印结果,可以发现:
我们可以看出来,前面的剩余时间都很正常,但是中间出现了一个50,这个是为什么?
因为浏览器可以发现,我们页面已经长时间没有发生过交互了,而对于人眼来说,响应交互的时延小于在50ms,也就是一秒20帧,人不会感觉明显的卡顿,所以浏览器会多给我们一些时间来执行这些优先级不高的任务。
如果我们把任务执行时间延长,比如一个任务需要1000ms,会发生什么?
const worksQueue = [
() => {
console . log('任务A开始')
sleep(20)
console . log('任务A结束')
},
() => {
console . log('任务B开始')
sleep(1000)
console . log('任务B结束')
},
() => {
console . log('任务C开始')
sleep(20)
console . log('任务C结束')
},
() => {
console . log('任务D开始')
sleep(1000)
console . log('任务D结束')
},
() => {
console . log('任务E开始')
sleep(20)
console . log('任务E结束')
}
]
很明显,浏览器都会提醒我们,执行的时候时间超时了。
requestIdleCallback第二个参数
但是,因为这个任务是非常低优先级的,可能任务在多次渲染后,仍没有机会很执行。
所以我们还可以传递一个参数,timeout指定超时时间,如果超时了(太长时间还没执行该任务),那么我们也去执行这些低优先级的任务。
如果didTimeout为ture,表明当前任务正在执行,且上次因为超时没有执行该任务
const sleep = (time) => {
const now = Date . now()
while (Date . now() < now + time) { }
}
const worksQueue = [
() => {
console . log('任务A开始')
sleep(20)
console . log('任务A结束')
},
() => {
console . log('任务B开始')
sleep(20)
console . log('任务B结束')
},
() => {
console . log('任务C开始')
sleep(20)
console . log('任务C结束')
},
() => {
console . log('任务D开始')
sleep(20)
console . log('任务D结束')
},
() => {
console . log('任务E开始')
sleep(20)
console . log('任务E结束')
}
]
const doSomething = (idleDeadline) => {
// idleDeadline.didTimeout // 是否过期 过期了就强制执行该任务 // idleDeadline.timeRemaining() // 返回值是剩余时间 while ((idleDeadline. timeRemaining() > 0 || idleDeadline.didTimeout) && worksQueue.length) {
console . log(idleDeadline.didTimeout)
console . log(idleDeadline. timeRemaining())
const work = worksQueue. shift()
work()
}
// 还有任务 if (worksQueue.length) requestIdleCallback(doSomething, { timeout: 10 })
}
requestIdleCallback(doSomething, { timeout: 100 })