為什麼要談動畫效能#
網頁上的動畫卡頓會直接破壞使用體驗,使用者就算沒有立刻離開,心裡也已經給網站扣分。本章接續高效能 CSS 的話題,把焦點放在「如何讓動畫流暢」上,並引入一個關鍵概念——硬體加速。
用 transform 取代直接改 style#
這個原則前面提過好幾次:若想讓元素移動,使用 transform 而非直接改 width、height、top、left 這類屬性。原因在於 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:
transformfilteropacity
但寫了這些屬性「並不保證」一定會啟用 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 分數上,但會直接決定使用者對網站的觀感。把這些技巧內化成肌肉記憶,寫動畫時就能少踩很多坑。