解读requestAnimationFrame

requestAnimationFrameFPS

大部分显示器的刷新率是60次/秒,也就是画面维持在60fps时人眼会觉得很流畅。浏览器的页面也是一帧一帧渲染出来的,因此要保证页面流畅,每帧渲染的的时间间隔不应该超过1000 / 60 ≈ 16.7ms,如果连续三帧低于24FPS,则视为出现了卡顿。当然我玩LOL的时候低于50fps都觉得卡的一比,所以才买了凄惨虹3070

requestAnimationFrameMDN解释:

window.requestAnimationFrame()  告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。这将使浏览器在下一次重绘之前调用你传入给该方法的动画函数(即你的回调函数)。回调函数执行次数通常是每秒60次。raf会把dom的变化集中操作,避免重复绘制。

raf注册的回调会在每次浏览器重绘之前执行,次数通常与浏览器屏幕刷新次数相匹配,意思我60fps的显示器,每隔16.67ms就会刷新一次,那么我们可以在回调的里面计算当前FPS

// 如果连续三次绘制都低于24帧/秒,则视为卡顿
  const smoothThreshold = 1000 / 24;
  function calcFps() {
    let now = performance.now();
    let frame = 0;
    let fps = 0;
    function checkFps() {
      frame++;
      if (frame >= 3) {
        console.log("页面出现了掉帧");
      }
      // 如果渲染下一帧超过流畅阈值,视为卡顿
      if (performance.now() - now < smoothThreshold) {
        fps = (frame * 1000) / (performance.now() - now);
        frame = 0;
      }
      now = performance.now();
      log(fps);
      window.requestAnimationFrame(checkFps);
    }
    requestAnimationFrame(checkFps);
  }
  window.requestAnimationFrame(calcFps);
  
   // 节流控制打印频率
  function log (fps) {
      return throttle(() => console.log(fps));
  }
 
  function throttle(fn, timeout = 600) {
    let timer = null;
    let now = performance.now();
    return (...args) => {
      clearTimeout(timer);
      timer = setTimeout(() => {
        if (performance.now() - now >= timeout) {
          fn.call(null, ...args);
          now = performance.now();
        }
      }, Math.max(timeout - (performance.now() - now)));
    };
  }
复制代码

为什么不使用setTimeout计算fps?

有以下两个原因:

  • 因为setTimeout受事件队列的影响,回调的执行时机要在任务栈清空后才会压栈,这就导致setTimeout回调执行时机往往会比设置的间隔时间要晚,而requestAnimationFrame的回调是由浏览器调度的,在绘制帧前会自动执行,不存在精度问题。

  • 不同设备的屏幕绘制频率可能会不同,比如120hz的屏幕绘制时间就是8.4ms, 而 setTimeout 只能设置一个固定的时间间隔,这个时间不一定和屏幕的刷新时间相同。

使用requestAnimationFramesetTimeout做动画

实现动画的方式可以通过css或者js,先讨论js做动画的一个表现情况。其中比较requestAnimationFramesetTimeout谁更合适做动画。下面使用两个小球做匀速运动,1号使用setTimeout, 2号球用raf

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Document</title>
    <style>
      .span2,
      .span4 {
        color: white;
        text-align: center;
        line-height: 30px;
        height: 30px;
        width: 30px;
        background-color: red;
        border-radius: 50%;
        margin-bottom: 50px;
      }
    </style>
  </head>

  <body>
    <div class="span2">1</div>
    <div class="span4">2</div>
    <script>
      // 设定为60帧
      const threshold = 1000 / 60;
      const duration = 5000;
      // 每一帧向前挪动距离
      const step = 1000 / (duration / threshold);
      
      const span2 = document.querySelector(".span2");
      const span4 = document.querySelector(".span4");

      let n = performance.now();

      function moveSetTimeout(el, count = 0) {
        setTimeout(() => {
          count++;
          // 测试回调执行时机是否会比设置的间隔时间要晚
          if (performance.now() - n > 20) {
            console.log("setimeout callback trigger:", performance.now() - n);
          }
          n = performance.now();
          el.style.transform = `translateX(${count * step}px)`;
          if (count * step < 1000) {
            moveSetTimeout(el, count);
          }
        }, threshold);
      }

      function moveRaf(el, count = 0) {
        const run = () => {
          count++;
          el.style.transform = `translateX(${count * step}px)`;
          if (count * step < 1000) {
            window.requestAnimationFrame(run);
          }
        };
        window.requestAnimationFrame(run);
      }

      moveSetTimeout(span2);
      window.requestAnimationFrame(() => moveRaf(span4));
    </script>
  </body>
</html>

复制代码

运动动图:

image.png

image.png

可以看得出raf2号小球运动平滑,而1号球出现了抖动丢帧的情况,而且1号球位移的距离比2号球要落后一些,位置上并没有重合。控制台多次打印。接下来分析下原因

原因分析

  • 为什么定时器回调执行时机不准
  • 为什么会出现抖动

为什么定时器回调执行时机不准

因为受event loop影响,即使定时器设置了16.67ms的间隔,但是因为定时器回调是宏任务,同步任务栈清空之前,宏任务不会被压栈,当主线程执行同步任务时间过长,定时器的回调执行时间也会被延后,这就导致了回调执行的间隔大于16.67ms

为什么会出现抖动

因为setTimeout操作的dom变化,会在浏览器下一次重绘之前执行,否则只会停留在内存中。而定时器回调因为无法保证跟浏览器重绘时间重合,会导致某一帧没有绘制,直接绘制下一帧,出现跳跃的情况,所以才会抖动。 下面举例每一帧 让dom每帧向左移动3.3px;

const threshold = 1000 / 60;
const duration = 5000;
// 每一帧向前挪动距离
const step = 1000 / (duration / threshold); // 3.33px
复制代码
// 定时器回调每次被触发的真实时间间隔
setimeout callback call: 17.79
setimeout callback call: 21.60
setimeout callback call: 16.79
setimeout callback call: 16.79
setimeout callback call: 19.29
复制代码

根据上面控制台的定时间间隔做下面表格。

时间 / 类型 setTimeout偏移距离 raf偏移距离 浏览器是否绘制
0ms 0 0
16.7ms 0 向左偏移3.3px
17.79ms 回调执行,设置向左偏移3.3px,等待下次绘制 0
33.4ms 向左偏移3.3px 向左偏移3.3px
39.39 回调执行,设置向左偏移3.3px,等待下次绘制 0
50.10ms 向左偏移3.3px 向左偏移3.3px
56.18ms 回调执行,设置向左偏移3.3px,等待下次绘制 0
66.80ms 向左偏移3.3px 向左偏移3.3px

根据上面的表格可以看得出,浏览器渲染第四帧的时候,定时器才渲染了三帧的位移,而raf保持一致,后面偏差会慢慢偏大,会出现定时器某一帧没有渲染,直接渲染后面某一帧,出现跳跃问题。因此尽量不要使用定时器做动画,如果要使用js做动画,应该使用raf

定时器和raf同异

共同点

  • 注册的回调都是宏任务,受event-loop管控
  • 运行在后台标签页或者隐藏的iframe里时,会被暂停调用以提升性能和电池寿命

为什么说受event-loop管控,看下下面代码

  moveSetTimeout(span2);
  window.requestAnimationFrame(() => moveRaf(span4));
  setTiemout(() => {
      const now = performance.now();
      // js线程直接挂起2000ms
      while(performance.now() - now < 2000) {}
  }, 1000)

复制代码

两个小球动会在1s后暂停2s, 然后继续动画,这是因为js任务栈被挂起两秒,而js线程和浏览器UI线程是互斥,raf和定时器的回调一直处于等待中,直到js线程中的任务栈被清空。因此即使使用raf做动画需要考虑这种场景,否则raf也可以做出很卡的动画。

差异

  • raf可以确保该回调函数会在浏览器下一次重绘之前执行,而定时器回调执行时机跟浏览器绘制不同步
  • raf可以智能跟随屏幕刷新率来确保回调执行,而定时器需要手动设置时间间隔
  • raf需要IE10以上版本支持,定时器无限制

动画尽量使用css3替代js

用Css3代替js做动画有以下好处:

  1. 脱离js线程影响(首次渲染后),即使js线程挂起了,并不影响动画, 因为动画运行在合成线程上
  2. 可以利用硬件GPU加速
  3. css3动画不会触发浏览器重绘重排

关于详细说明可以查看这里:主线程和合成线程

关于第一点可以用下面代码验证: JS线程挂起了2秒,并不影响动画的渲染。

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Document</title>
    <style>
        .span1 {
            height: 30px;
            display: inline-block;
            width: 30px;
            background-color: red;
            border-radius: 50%;
            margin-bottom: 20px;
            animation: 5s linear move;
            color: white;
            text-align: center;
            line-height: 30px;
        }
        @keyframes move {
            0% {
                transform: translateX(0);
            }

            100% {
                transform: translateX(calc(100vw));
            }
        }
    </style>
</head>

<body>
    <span class="span1">1</span>
    <script>
        setTimeout(() => {
            let now = performance.now();
            while(performance.now() - now < 2000){}
        }, 1000)
    </script>
</body>

</html>
复制代码

这也是为什么即使JS主线程卡住了,CSS 动画依然能执行。

第三点可以通过peformance面板录制查看

image.png

总结

通过分析比较,更好的了解raf和定时器的做动画的差异,顺带复习了下event-loop和浏览器线程。

参考:

Supongo que te gusta

Origin juejin.im/post/7031132507479212063
Recomendado
Clasificación