CSS 和 JavaScript 动画性能

动画对于许多应用程序中愉悦的用户体验至关重要。实现 Web 动画有多种方法,例如 CSS transitions/animations 或基于 JavaScript 的动画(使用 requestAnimationFrame())。在本文中,我们将分析基于 CSS 和基于 JavaScript 的动画之间的性能差异。

CSS 过渡和动画

CSS 过渡和动画都可以用于编写动画。它们各有其用户场景。

  • CSS transitions 提供了一种简单的方法,可以在当前样式和最终 CSS 状态之间实现动画,例如,按钮的静止状态和悬停状态。即使元素正处于过渡动画中,新的过渡也会立即从当前样式开始,而不是跳转到最终的 CSS 状态。有关更多详细信息,请参阅 使用 CSS 过渡
  • 另一方面,CSS animations 允许开发人员在一组起始属性值和一组最终属性值之间创建动画,而不是在两个状态之间。CSS 动画包含两个组件:描述 CSS 动画的样式,以及一组关键帧,这些关键帧指示动画样式的开始和结束状态以及可能的中间点。有关更多详细信息,请参阅 使用 CSS 动画

在性能方面,使用 CSS 过渡或动画实现动画之间没有区别。在本文中,它们都归类在相同的“基于 CSS”的类别下。

requestAnimationFrame

requestAnimationFrame() API 提供了一种在 JavaScript 中实现动画的高效方法。该方法的调用函数在每一帧的下一次重绘之前由浏览器调用。与需要特定延迟参数的 setTimeout()/setInterval() 相比,requestAnimationFrame() 效率更高。开发人员可以通过在每次调用循环时更改元素的样式(或更新 Canvas 绘制,或其他任何内容)来创建动画。

注意:与 CSS 过渡和动画一样,当当前选项卡被推到后台时,requestAnimationFrame() 会暂停。

有关更多详细信息,请阅读 使用 JavaScript 从 setInterval 到 requestAnimationFrame 进行动画处理

性能比较
Transitions vs. requestAnimationFrame

事实上,在大多数情况下,尤其是在 Firefox 中,基于 CSS 的动画的性能与基于 JavaScript 的动画几乎相同。一些基于 JavaScript 的动画库,如 GSAPVelocity.JS,甚至声称它们能够实现比 原生 CSS 过渡/动画 更好的性能。这种情况之所以会发生,是因为 CSS 过渡/动画在每次重绘事件发生之前会在主 UI 线程中重新采样元素的样式,这几乎等同于通过 requestAnimationFrame() 回调(也在下一次重绘之前触发)重新采样元素的样式。如果两个动画都在主 UI 线程中进行,那么在性能上就没有区别。

在本节中,我们将通过一个使用 Firefox 的性能测试来逐步介绍,看看哪种动画方法总体上似乎更好。

启用 FPS 工具

在进行示例之前,请先启用 FPS 工具以查看当前的帧率。

  1. 在 URL 栏中输入 _about:config_;点击“我将要小心,我保证!”按钮进入配置屏幕。警告屏幕,提示更改设置可能存在风险,并有一个接受风险的按钮。
  2. 在搜索栏中,搜索 _layers.acceleration.draw-fps_ 首选项。
  3. 双击该条目将其值设置为 true。现在您将在 Firefox 窗口的左上角看到三个小的紫色方块。第一个方块代表 FPS。输入搜索词会过滤选项。仅显示 layers.acceleration.draw-fps 首选项,并设置为 true。浏览器左上角出现三个数字(001、001 和 108),叠加在其 UI 上。

运行性能测试

在下面的测试中,最初通过 CSS 动画转换了总共 1000 个 <div> 元素。

js
const boxes = [];
const button = document.getElementById("toggle-button");
const boxContainer = document.getElementById("box-container");
const animationType = document.getElementById("type");

// create boxes
for (let i = 0; i < 1000; i++) {
  const div = document.createElement("div");
  div.classList.add("css-animation");
  div.classList.add("box");
  boxContainer.appendChild(div);
  boxes.push(div.style);
}

let toggleStatus = true;
let rafId;
button.addEventListener("click", () => {
  if (toggleStatus) {
    animationType.textContent = " requestAnimationFrame";
    for (const child of boxContainer.children) {
      child.classList.remove("css-animation");
    }
    rafId = window.requestAnimationFrame(animate);
  } else {
    window.cancelAnimationFrame(rafId);
    animationType.textContent = " CSS animation";
    for (const child of boxContainer.children) {
      child.classList.add("css-animation");
    }
  }
  toggleStatus = !toggleStatus;
});

const duration = 6000;
const translateX = 500;
const rotate = 360;
const scale = 1.4 - 0.6;
let start;
function animate(time) {
  if (!start) {
    start = time;
    rafId = window.requestAnimationFrame(animate);
    return;
  }

  const progress = (time - start) / duration;
  if (progress < 2) {
    let x = progress * translateX;
    let transform;
    if (progress >= 1) {
      x = (2 - progress) * translateX;
      transform = `translateX(${x}px) rotate(${
        (2 - progress) * rotate
      }deg) scale(${0.6 + (2 - progress) * scale})`;
    } else {
      transform = `translateX(${x}px) rotate(${progress * rotate}deg) scale(${
        0.6 + progress * scale
      })`;
    }

    for (const box of boxes) {
      box.transform = transform;
    }
  } else {
    start = null;
  }
  rafId = window.requestAnimationFrame(animate);
}

通过单击切换按钮,可以将动画切换到 requestAnimationFrame()

现在尝试同时运行它们,比较每种动画的 FPS(第一个紫色方块)。您应该会看到 CSS 动画和 requestAnimationFrame() 的性能非常接近。

在主线程之外进行动画

即使考虑到上述测试结果,我们也认为 CSS 动画是更好的选择。但为什么呢?关键在于,只要我们要动画的属性不会触发重排/重绘(有关更多信息,请阅读 CSS Triggers),我们就可以将那些采样操作移出主线程。最常见的属性是 CSS transform。如果一个元素被提升为一个 图层,那么 transform 属性的动画就可以在 GPU 上完成,这意味着更好的性能/效率,尤其是在移动设备上。有关更多详细信息,请参阅 OffMainThreadCompositing

要在 Firefox 中启用 OMTA(Off Main Thread Animation),您可以转到 _about:config_ 并搜索 _layers.offmainthreadcomposition.async-animations_。将其值切换为 true

Entering the search term filters the options. Only the layers.offmainthreadcomposition.async-animations preference is showing, set to true. The three numbers in the upper left corner of the browser, above its UI, have increased to 005, 003, and 108.

启用 OMTA 后,请再次尝试运行上面的测试。您应该会发现 CSS 动画的 FPS 现在会明显更高。

注意:在 Nightly/Developer Edition 中,您会发现 OMTA 默认启用,因此您可能需要反过来进行测试(先启用 OMTA 进行测试,然后禁用它以在没有 OMTA 的情况下进行测试)。

总结

浏览器能够优化渲染流程。总而言之,我们应该尽可能地使用 CSS 过渡/动画来创建动画。如果您的动画非常复杂,您可能需要依赖基于 JavaScript 的动画。