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