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 的動畫。