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 的动画库,如 GSAP 和 Velocity.JS,甚至声称它们能够实现比 原生 CSS 过渡/动画 更好的性能。这种情况之所以会发生,是因为 CSS 过渡/动画在每次重绘事件发生之前会在主 UI 线程中重新采样元素的样式,这几乎等同于通过 requestAnimationFrame()
回调(也在下一次重绘之前触发)重新采样元素的样式。如果两个动画都在主 UI 线程中进行,那么在性能上就没有区别。
在本节中,我们将通过一个使用 Firefox 的性能测试来逐步介绍,看看哪种动画方法总体上似乎更好。
启用 FPS 工具
在进行示例之前,请先启用 FPS 工具以查看当前的帧率。
- 在 URL 栏中输入 _about:config_;点击“
我将要小心,我保证!
”按钮进入配置屏幕。 - 在搜索栏中,搜索 _layers.acceleration.draw-fps_ 首选项。
- 双击该条目将其值设置为
true
。现在您将在 Firefox 窗口的左上角看到三个小的紫色方块。第一个方块代表 FPS。
运行性能测试
在下面的测试中,最初通过 CSS 动画转换了总共 1000 个 <div>
元素。
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
。
启用 OMTA 后,请再次尝试运行上面的测试。您应该会发现 CSS 动画的 FPS 现在会明显更高。
注意:在 Nightly/Developer Edition 中,您会发现 OMTA 默认启用,因此您可能需要反过来进行测试(先启用 OMTA 进行测试,然后禁用它以在没有 OMTA 的情况下进行测试)。
总结
浏览器能够优化渲染流程。总而言之,我们应该尽可能地使用 CSS 过渡/动画来创建动画。如果您的动画非常复杂,您可能需要依赖基于 JavaScript 的动画。