# 如何才能更好的压榨浏览器?
各位点进来的兄弟姐妹, 浏览器这么兢兢业业的为你服务, 为何你还要想方设法去压榨它, 你的内心不会觉得愧疚吗?!
好了, 开个玩笑, 言归正传, 我们今天的主角是 requestAnimationFrame
和 requestIdleCallback
.那么它们和压榨浏览器有什么关系呢?借助它们我们可以深入浏览器内部渲染的生命周期, 利用好一切我们可以利用的时间, 榨干浏览器的每分每秒.
# requestAnimationFrame
首先我们来说说 requestAnimationFrame
, 从这个名字就可以看出来, 它可以用在动画上, 我们先来考虑这样一个场景, 用js
实现一个div
向右滑动的动画, 我们第一反应可能是利用 setInterval
<!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>
#animation {
width: 100px;
height: 100px;
background-color: red;
position: absolute;
}
</style>
</head>
<body>
<div id="animation" style="left: 10px"></div>
<script>
setInterval(() => {
const div = document.querySelector('#animation');
const left = div.getBoundingClientRect().left;
div.style.left = `${left+2}px`;
}, 100);
</script>
</body>
</html>
so easy, 我们来看一眼效果
看起来其实还行?但是如果你仔细看的话就会发现, 它有一点不自然, 有一种掉帧的感觉.这是为什么呢, 因为如果帧数是60fps, 那么就是1s内刷新60次, 也就是 1/60 s刷新一次, 但是我们的时间设置为了100ms, 这个时间和浏览器的刷新时间对不上, 所以造成了一种动画不够流畅的感觉, 我们可以调整间隔, 但是最好的方式就是让浏览器自己控制自己, 让我们的函数每一帧只执行一次, 因为执行多了没必要, 执行少了不行, 一次刚刚好.requestAnimationFrame
就是用来干这个事情的, 它接受一个函数, 这个函数一帧内只会被调用一次, 调用时间由浏览器决定, 所以我们把代码改一下.
<!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>
#animation {
width: 100px;
height: 100px;
background-color: red;
position: absolute;
}
</style>
</head>
<body>
<div id="animation" style="left: 10px"></div>
<script>
// setInterval(() => {
// const div = document.querySelector('#animation');
// const left = div.getBoundingClientRect().left;
// div.style.left = `${left+2}px`;
// }, 100);
function animation() {
requestAnimationFrame(() => {
const div = document.querySelector('#animation');
const left = div.getBoundingClientRect().left;
div.style.left = `${left+2}px`;
animation();
})
}
animation();
</script>
</body>
</html>
因为每次只会执行一次, 所以我们需要递归调用这个函数, 我们再来看看效果
怎么样, 是不是感觉流畅很多, 脏活累活丢给浏览器干就行了, 用setInterval
还得算算间隔多少比较合适, 没必要, requestAnimationFrame
简单又实用.
# requestIdleCallback
下面我们来说说 requestIdleCallback
, 它的作用是什么呢?我们知道, 浏览器的渲染是有一套流程的, 比如A、B、C、D四个步骤, 一次渲染就是A->B->C->D, 然后我们就能在界面上看见渲染出来的东西了, 想了解具体的内容可以藏考我之前写的一篇文章 (opens new window).那如果浏览器1s渲染60帧, 一帧花费的时间是 1/60 s, 结果还没花完, 活就干完了, 这咋办.浏览器表示: 那不是刚好摸鱼吗?那资本家会放轻易过你吗?不行, 每一秒都得压榨干净, 你没事干了我可以给事情给你干, requestIdleCallback
, 通过它我们就能给浏览器派更多的活, 让它在这种空闲时间有事情可干.
requestIdleCallback
接受一个函数作为参数, 这个函数会在浏览器空闲的时候被调用, 同时这个函数还接受一个参数, 更具体的用法还是参考MDN (opens new window), 我这里就不作更多的介绍了.那通过它能干什么?我们知道react
更新了fiber
架构, 这是个什么玩意, 简单来说, 以前如果一个组件非常庞大, 下面的子组件非常多, 它也只能一次性渲染完, 无法中断.此时如果来了其他的任务, 就会被阻塞, 所以在用户看来某些任务无法及时得到响应, 那就是界面一卡一卡的, 掉帧了.
而更新了fiber
架构之后呢, react
可以做到中断更新, 去响应一些优先级更高的任务, 我们举一个简单的小例子:
<!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>
<script>
const taskList = [
{
id: 1,
msg: 'first task'
},
{
id: 2,
msg: 'second task'
},
{
id: 3,
msg: 'third task'
},
{
id: 4,
msg: 'four task'
}
]
function wait(time) {
const now = Date.now();
while (Date.now() - now < time) {}
}
function oldExecuteTask(list = taskList) {
list.forEach(item => {
console.log("execute task", item.msg);
wait(1000);
})
}
function newExecuteTask(list = taskList) {
requestIdleCallback(() => {
const firstList = list[0];
console.log(`execute task ${firstList.msg}`);
wait(1000);
list.length > 1 && newExecuteTask(list.slice(1));
}, { timeout: 2000 })
}
</script>
</head>
<body>
<button onclick="oldExecuteTask()">execute old task</button>
<button onclick="newExecuteTask()">execute new task</button>
<button onclick="(() => {console.log('other task')})()">other task</button>
</body>
</html>
此时我们有一个任务队列taskList
, 当我们执行这个任务队列时, 我们去点击other task
按钮, 此时因为之前的任务没有执行完, 老react
是无法响应的, 而新react
因为有中断机制所以可以响应, 我们首先看看老react
的表现
当无法中断时, 我疯狂点击按钮也不会有console
打出来, 当任务执行完毕后才打出了之前的console
, 而用了requestIdleCallback
之后我们每次只执行一个任务, 下个任务再次通过requestIdleCallback
来调用, 保证执行我们任务的同时还能响应一些其他的任务, 我们看效果
可以看到, 我们在执行任务的同时还能响应其他的任务, 这就和react
的新架构类似.所以, 通过requestIdleCallback
, 我们就能更好的利用好各种碎片时间来执行优先级不那么高的任务, 不要让浏览器白白浪费掉多余的时间.
# 结语
其实, 在我看来, 这两个api给我们提供了深入浏览器内部生命周期的机会, 也就是给了我们很大的发挥空间, 让我们能够能够更好的去压榨浏览器了.目前来看requestIdleCallback
的兼容性还不是非常好, 所以希望用在实际生产环境的同学还需要仔细调研一下.
跨端杂谈 →