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 进行动画

性能比较
过渡与 requestAnimationFrame

事实上,在大多数情况下,基于 CSS 的动画的性能几乎与基于 JavaScript 的动画相同 - 至少在 Firefox 中是这样。一些基于 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 触发器 以了解更多信息),我们就可以将这些采样操作移出主线程。最常见的属性是 CSS transform。如果一个元素被提升为 ,则可以在 GPU 中对 transform 属性进行动画,这意味着更好的性能/效率,尤其是在移动设备上。在 非主线程合成 中可以找到更多详细信息。

若要启用 Firefox 中的 OMTA(非主线程动画),您可以转到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 测试)。

总结

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