為什麼要談動畫效能#

網頁上的動畫卡頓會直接破壞使用體驗,使用者就算沒有立刻離開,心裡也已經給網站扣分。本章接續高效能 CSS 的話題,把焦點放在「如何讓動畫流暢」上,並引入一個關鍵概念——硬體加速。

用 transform 取代直接改 style#

這個原則前面提過好幾次:若想讓元素移動,使用 transform 而非直接改 widthheighttopleft 這類屬性。原因在於 transform 不會觸發 reflow 與 repaint,效能成本最低。同樣的觀念在這裡再度成立,是因為 transform 還能搭配下一節的硬體加速一起發揮。

Hardware Acceleration 硬體加速#

硬體加速簡單說就是:把比較複雜的渲染計算交給 GPU 處理,而不是全部都壓在 CPU 上。某些動畫透過 GPU 加速(GPU Acceleration)後會明顯更順暢。

GPU 是什麼#

GPU(Graphics Processing Unit,圖形處理器)位於顯示卡上,專門處理圖形渲染所需的大量幾何與數學計算。CPU 是電腦的大腦,但圖形相關運算未必擅長;GPU 則是為了這類工作設計的,把對應的負擔卸給它,整體效能與流暢度都會提升,行動裝置上感受最明顯。

與分層的關係#

當頁面元素執行某些操作(例如 3D transform)時,瀏覽器會把這個元素提升到一個獨立的合成層(Compositing Layer)。後續的 raster 與 composite 流程在這個層獨立進行,不會牽動其他元素。

加上現代瀏覽器的 GPU 行程獨立於渲染引擎,這層工作也不會 block 其他渲染任務。多個前提疊加起來,動畫效能就能拉開差距。

確認瀏覽器是否啟用硬體加速#

寫得再好,瀏覽器若沒開啟硬體加速也是白搭。以 Chrome 為例:

  • 進入「設定」➡️「系統」,確認「在可用時使用硬體加速」是開啟狀態(預設應該開啟)。
  • 在網址列輸入 chrome://gpu,可以看到目前哪些功能是用 GPU 加速處理的。

哪些 CSS 屬性「有機會」被 GPU 加速#

部分 CSS 屬性被歸類為 GPU accelerated properties:

  • transform
  • filter
  • opacity

但寫了這些屬性「並不保證」一定會啟用 GPU 加速。瀏覽器會自行判斷,例如 2D transform 通常不會啟用,3D transform 才會強制使用硬體加速。

transform hack:把 2D 偽裝成 3D#

這就引申出一個常見的小技巧:把純 2D 的位移改寫成 3D 形式,讓瀏覽器啟用合成層(Compositing Layer)。原本的:

transform: translate(0, 20px);

改寫成:

transform: translate3d(0, 20px, 0);

或使用 translateZ(0) 等技巧。背後機制是強迫該元素獲得獨立的合成層,並由 GPU 負責 composite。

transform hack 不是免費的午餐。建立獨立合成層會消耗記憶體,行動裝置上特別明顯,過量使用甚至會讓瀏覽器直接 crash。要在「真的能換來效能提升」時才用。

更好的選擇:will-change#

will-change 是後來新增的 CSS 屬性,用途是「提示」瀏覽器:這個元素未來可能會做哪些變化,請先做好準備。

will-change: transform;

/* 也可以同時提示多個 */
will-change: transform, opacity;

相較於 transform hack 強迫建立合成層,will-change 把選擇權交回給瀏覽器,由它決定最佳的優化方式(可能建立合成層,也可能採取其他策略),避免不必要的副作用。

使用 will-change 的注意事項#

  • 不要套用在不會變動的元素上。最糟的反例:
* {
  will-change: transform;
}

這等於告訴瀏覽器「整個頁面的元素都可能變動」,瀏覽器除了無從判斷優先順序外,還得花額外資源處理,反而把頁面拖垮。

  • 動畫結束後記得移除。will-change 帶來的優化通常成本不低,會佔住資源。對於只觸發一次的動畫,動畫結束後應透過 JavaScript 移除提示:
const el = document.getElementById("puddydat");

el.addEventListener("mouseenter", addHint);
el.addEventListener("animationend", removeHint);

function addHint() {
  this.style.willChange = "transform";
}

function removeHint() {
  this.style.willChange = "auto";
}
  • 對於持續性的動畫(例如跟隨滑鼠移動的元素、不斷彈跳的提示),可以保留 will-change,因為效益持續存在。

Demo:差異有多大#

把一個會大量重排的列表當作測試標的。第一種寫法直接改 style.top

document.querySelector("#reorderBtn").addEventListener("click", () => {
  elementRows.reverse();
  elementRows.forEach((eRow, rowIndex) => {
    eRow.style.top = rowIndex * ROW_HEIGHT + "px";
  });
});

第二種寫法改用 transform: translate,並加上 will-change

.row {
  height: 25px;
  box-sizing: border-box;
  position: absolute;
  width: 100%;
  will-change: transform;
  transition: transform 0.5s;
}
document.querySelector("#reorderBtn").addEventListener("click", () => {
  elementRows.reverse();
  elementRows.forEach((row, rowIndex) => {
    row.style.transform = "translate(0, " + rowIndex * ROW_HEIGHT + "px)";
  });
});

兩個版本實際跑起來的順暢度差異非常顯著,越複雜的列表差異越大。

一行 CSS 摧毀一個網站#

如果不留情地對所有元素套用 will-change: transform,頁面初始渲染時間就會被拖長,連基本互動都會受影響。這個反例提醒我們:合成層(Compositing Layer)不是越多越好,要讓瀏覽器有判斷空間。

小結#

CSS 動畫效能優化的關鍵心法只有兩條:

  • 用 transform / opacity 來做動畫,避開 reflow 與 repaint。
  • 需要 GPU 加速時,先嘗試 will-change,且只用在「真的會變」的元素,動畫結束後記得移除。

最後一個提醒:動畫的卡頓未必會反映在 Lighthouse 分數上,但會直接決定使用者對網站的觀感。把這些技巧內化成肌肉記憶,寫動畫時就能少踩很多坑。

原文出處#