如何才能更好的压榨浏览器?

573 阅读2分钟

各位点进来的兄弟姐妹, 浏览器这么兢兢业业的为你服务, 为何你还要想方设法去压榨它, 你的内心不会觉得愧疚吗?!

好了, 开个玩笑, 言归正传, 我们今天的主角是 requestAnimationFramerequestIdleCallback 。那么它们和压榨浏览器有什么关系呢?借助它们我们可以深入浏览器内部渲染的生命周期, 利用好一切我们可以利用的时间, 榨干浏览器的每分每秒。

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, 我们来看一眼效果 setInterval.gif 看起来其实还行?但是如果你仔细看的话就会发现, 它有一点不自然, 有一种掉帧的感觉。这是为什么呢,因为如果是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>

因为每次只会执行一次, 所以我们需要递归调用这个函数, 我们再来看看效果

requestAnimation.gif

怎么样, 是不是感觉流畅很多, 脏活累活丢给浏览器干就行了, 用setInterval 还得算算间隔多少比较合适, 没必要, requestAnimationFrame简单又实用。

requestIdleCallback

下面我们来说说 requestIdleCallback,它的作用是什么呢?我们知道,浏览器的渲染是有一套流程的,比如A、B、C、D四个步骤,一次渲染就是A->B->C->D,然后我们就能在界面上看见渲染出来的东西了,想了解具体的内容可以藏考我之前写的一篇文章。那如果浏览器1s渲染60帧,一帧花费的时间是 1/60 s,结果还没花完,活就干完了,这咋办。浏览器表示:那不是刚好摸鱼吗?那资本家会放轻易过你吗?不行,每一秒都得压榨干净,你没事干了我可以给事情给你干,requestIdleCallback,通过它我们就能给浏览器派更多的活,让它在这种空闲时间有事情可干。

requestIdleCallback 接受一个函数作为参数,这个函数会在浏览器空闲的时候被调用,同时这个函数还接受一个参数,更具体的用法还是参考MDN,我这里就不作更多的介绍了。那通过它能干什么?我们知道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因为有中断机制所以可以响应.我们先看看老的效果

task-old.gif

可以看到,当无法中断时,我疯狂点击按钮也不会有console打出来,当任务执行完毕后才打出了之前的console,而用了requestIdleCallback之后我们每次只执行一个任务,下个任务再次通过requestIdleCallback来调用,保证执行我们任务的同时还能响应一些其他的任务,我们看效果

new-task.gif

可以看到,我们在执行任务的同时还能响应其他的任务,这就和react的新架构类似。所以,通过requestIdleCallback,我们就能更好的利用好各种碎片时间来执行优先级不那么高的任务,不要让浏览器白白浪费掉多余的时间。

结语

其实,在我看来,这两个api给我们提供了深入浏览器内部生命周期的机会,也就是给了我们很大的发挥空间,让我们能够能够更好的去压榨浏览器了。目前来看requestIdleCallback的兼容性还不是非常好,所以希望用在实际生产环境的同学还需要仔细调研一下。