requestAnimationFrame 和 setTimeout 的区别

138 阅读5分钟

requestAnimationFrame 和 setTimeout 的区别

设计目的与基本概念

  • requestAnimationFrame ​​是浏览器专门为高性能动画设计的 API,其核心目标是​​与浏览器渲染周期同步​​,实现流畅的视觉效果。

    • 当调用 requestAnimationFrame 时,实际上是告诉浏览器:"我希望在下一次重绘之前执行这个回调函数来更新动画"。
    • 这种方法确保动画更新与屏幕刷新节奏完美同步,通常以 60Hz(约 16.7ms/帧)的频率运行
    • requestAnimationFrame 是​​声明式​​的("请在下次绘制前调用我")
  • setTimeou t​​则是 JavaScript 中通用的延时执行工具,用于在​​指定的毫秒数后执行一次回调函数​​(setTimeout)或​​重复执行​​(setInterval)。

    • 它不关心浏览器的渲染周期,只是简单地将任务推入异步队列等待执行。
    • 例如,setTimeout(callback, 16)试图近似 60FPS 的动画效果,但无法保证精确的时间控制。
    • setTimeout 是​​命令式​​的("在 16 毫秒后调用我,不管系统状态如何")

事件循环与调度机制

  • setTimeout 的执行依赖于 JavaScript 的​​事件循环机制​​。

  • 当调用 setTimeout(callback, delay)时,计时器开始计时,但回调函数不会在计时结束后立即执行,而是被放入​​任务队列​​。只有当调用栈为空时,事件循环才会从任务队列中取出这个回调执行。 这意味着:

    • ​实际执行时间可能远晚于预期​​:如果主线程有长时间运行的同步代码,setTimeout 回调必须等待,导致动画卡顿。
    • 时间间隔不精确​​:即使设置为 16.7ms(60FPS),由于系统负载和事件循环的不可预测性,实际间隔可能在 16.7-100ms 甚至更长。
    • 累积误差​​:连续的 setTimeout 调用会因执行延迟而产生误差积累,导致动画越来越不同步。
  • 相比之下,requestAnimationFrame 的回调由​​浏览器渲染管线直接调度​​,在"渲染前"阶段执行。浏览器会自动将多个 requestAnimationFrame 回调合并到同一帧处理, 确保:

    • 与显示器刷新同步​​:60Hz 屏幕约 16.7ms 一帧,120Hz 屏幕约 8.3ms 一帧,自动适配。
    • 避免过度渲染​​:即使代码频繁调用,浏览器也只会按屏幕刷新率执行,不会产生多余的重绘。
    • 高精度时间戳​​:回调函数接收一个 DOMHighResTimeStamp 参数,精确表示触发时间(精度可达 1ms),便于实现基于时间的动画计算

后台标签页行为

当页面切换到后台或最小化时,两者的表现截然不同:

  • setTimeout/setInterval 会​​继续执行​​,消耗 CPU 资源,尽管这些更新对用户不可见。
  • requestAnimationFrame 会​​自动暂停​​,直到页面再次可见。 这不仅节省资源,还能在恢复时从断点继续,避免"跳帧"。这种差异在移动设备上尤为明显。 使用 setTimeout 的动画在后台运行时,会不必要地消耗电量,可能触发浏览器的节流策略,导致返回页面时动画异常

CPU/GPU 效率

requestAnimationFrame 的设计充分考虑了浏览器渲染优化:

  1. 批量 DOM 操作​​:同一帧内的所有 DOM 修改会被浏览器智能合并,减少重排(Reflow)和重绘(Repaint)次数。
  2. 空闲期优化​​:浏览器可以在帧之间的空闲期执行垃圾回收等任务,避免干扰动画。
  3. 硬件加速​​:现代浏览器会对 requestAnimationFrame 动画应用 GPU 加速,特别是 CSS 变换和 Canvas 操作。

而 setTimeout 动画容易引发​​布局抖动​​(Layout Thrashing)

  1. 频繁的样式读写迫使浏览器提前计算布局,导致性能下降。

测试表明,相同的动画效果,requestAnimationFrame 的 CPU 占用率通常比 setTimeout 低 2-3 倍。

帧率稳定性

通过 FPS(Frames Per Second)测试可以直观看到差异:

  • requestAnimationFrame:稳定在 60FPS(或屏幕刷新率),波动范围小。
  • setTimeout:FPS 波动大,尤其在系统负载高时可能出现明显卡顿,即使设置为 16.7ms 间隔,实际可能只有 30-45FPS。

适用场景对比

requestAnimationFrame 最适合​​:

  1. 连续动画​​:CSS 变换、Canvas 绘图、WebGL 渲染等需要流畅视觉效果的情况。
  2. 滚动相关操作​​:无限滚动、视差滚动、懒加载等,避免滚动事件的高频触发导致的性能问题。
  3. 游戏开发​​:游戏主循环需要稳定的帧率,与渲染同步。
  4. 大数据渲染​​:分批次将大量 DOM 元素插入文档,避免界面冻结

setTimeout/setInterval 更适合​​:

  1. 非视觉任务​​:延迟提示、轮询服务器、超时控制等。
  2. 一次性延迟​​:如弹窗延迟显示、工具提示延迟隐藏等。
  3. 不精确的重复任务​​:如每 5 分钟检查一次新邮件

验证执行逻辑

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <h2>requestAnimation 和 setTimeout 的区别</h2>
  </body>
  <script>
    const runFunc = (fn, isRequestAnimationFrame = true) => {
      if (isRequestAnimationFrame) {
        /**
         * requestAnimationFrame
         *
         * 如果页面在后台运行时,那么将不会执行
         * 等回到前台,才会继续执行
         *
         *
         * 打印 166ms
         *
         * 打印 200ms
         *
         * 后台不执行
         *
         * 回到前台
         * 打印 12000ms
         */
        requestAnimationFrame(fn);
      } else {
        /***
         * setTimeout
         *
         * 虽然规定的是 16.66ms 执行一次
         * 但是如果页面在后台运行,那么不会 16.66ms 执行一次
         * 浏览器会做优化,会在 1000ms 左右执行一次
         *
         * 打印 166ms
         *
         * 打印 200ms
         *
         * 后台
         *
         * 打印 1200ms
         * 打印 2200ms
         * 打印 3200ms
         * 打印 4200ms
         * 打印 5200ms
         * 打印 6200ms
         * xxxx 直到终止
         *
         */
        setTimeout(fn, 16.66);
      }
    };

    let startTime = null;
    function animate(timestamp) {
      console.log(new Date());
      const newTimestamp = timestamp || document.timeline.currentTime || performance.now();
      if (!startTime) startTime = newTimestamp;
      const progress = newTimestamp - startTime;
      console.log('🚀 ~ animate ~ progress:', progress);

      if (progress < 10000) {
        // 运行 2 秒
        // requestAnimationFrame(animate);
        runFunc(animate, false);
      }
    }
    // requestAnimationFrame(animate);
    runFunc(animate, false);
  </script>
</html>