不難發現,前幾天只提到用 transform、opacity、translate、rotate 等來寫動畫。當然 CSS 其他屬性也能做成動態效果,但若寫過多、過重會影響網頁效能,使用起來明顯會有卡頓的感覺。
今天,來研究動畫對網頁效能的影響:先來理解瀏覽器如何渲染畫面?為什麼有的 CSS 屬性輕、有的 CSS 屬性重?再到動畫寫得重會有什麼影響,最後再回頭看之前踩過哪些坑。
那我們開始吧!
瀏覽器要把畫面顯示出來,會依序走過四個步驟:
做動畫的時候,瀏覽器每一幀都要重新走一遍流程,而且是從「需要更新的那一步」開始,往後全部重跑。越前面的步驟被觸發,後面要做的事就越多。
依照動畫改了什麼屬性,成本大致分三種:
| 動了什麼 | 觸發的步驟 | 例子 | 成本 |
|---|---|---|---|
| 影響大小或位置 | Layout → Paint → Composite | width、height、left、top、margin、padding、font-size |
最高 |
| 只影響外觀 | Paint → Composite | color、background-color、box-shadow |
中 |
| 只需要合成 | 只有 Composite | transform、opacity |
最低 |
Layout 變動的整體成本最高。當一個元素的大小或位置改變,可能連帶影響旁邊元素的排列,瀏覽器得重新計算,範圍甚至可能一路往上到 DOM tree 的根節點,而且後面的 Paint 與 Composite 也得跟著重跑。若單看單一步驟,Paint 則往往是最耗時的一步,尤其是有陰影、漸層效果的畫面。相比之下,Composite 只是把已經畫好的圖層,換個位置或透明度再疊一次,輕量許多。
先來了解瀏覽器裡有兩條跟畫面有關的主要執行緒:
如果動畫是用 transform 和 opacity 屬性,就能在合成器執行緒處理。若只涉及 Reflow 或 Repaint,就得回到主執行緒處理,若主執行緒一忙,那麼動畫就跟著卡頓。
要讓方塊往右移動 100px,兩種寫法都做得到:
/* 方塊 A:動 left,需要 Layout + Paint + Composite */
.box-a { position: relative; animation: moveLeft 2s linear infinite alternate; }
@keyframes moveLeft {
from { left: 0; }
to { left: 100px; }
}
/* 方塊 B:動 transform,通常只需要 Composite */
.box-b { animation: moveTransform 2s linear infinite alternate; }
@keyframes moveTransform {
from { transform: translateX(0); }
to { transform: translateX(100px); }
}
畫面上看起來幾乎一樣,但每一幀背後的工作量差很多。可以到 Codepen 連結 操作看看,嘗試做出以下操作,得到的反饋分別是:
回頭檢查以前專案裡的寫法,有三個地方正好踩到前面說的雷。
will-change前面提到,transform 與 opacity 要真正只走合成,元素得在自己的 Composite Layer 上。Composite Layer 可以想成「獨立的一張透明片」,移動或調整透明度時,瀏覽器只要處理這張透明片,不用重畫整個畫面。
will-change 就是用來提示瀏覽器「我之後會動這個屬性,請先準備好」的寫法:
.deco {
will-change: transform;
}
聽起來很讚,但它不是越多越好,MDN 的建議是:
<body>)上。@keyframes 裡。因為瀏覽器本來就知道它們會變。再加上每個 Composite Layer 都要佔記憶體,層數太多反而讓效能變差。實際使用,應該等到真的發現某個動畫卡頓,再針對那一個元素加,並用 DevTools 驗證有沒有改善。
transition 沒有指定屬性transition 的 transition-property 都直接寫 all,就等於「這個元素上任何會變的屬性,都要做漸變」。雖然很方便,但之後只要有人在 :hover 裡多加了一個 width 或 margin,這個屬性也會跟著漸變,每一幀都得重新 Layout。原本只想做個簡單的位移,結果多了一個影響排版的動畫,卻很難察覺。
應該要明確寫出要漸變的屬性:
.cup {
transition: translate 0.3s ease, opacity 0.3s ease;
}
left、right、top、bottom 寫移動動畫之前有個 absolute 定位的元素要做移動動畫,因為同時也用了 transform: translate(-50%, -50%) 來對齊,所以直覺地改了它的 left、right、top、bottom,但這些都會觸發 Layout,也就是前面實測中方塊 A 的寫法。例如專案裡角色彈出的動畫,就是用 bottom 從畫面外一路動到定位點:
.cup {
position: absolute;
bottom: 20px;
}
/* 改前:動 bottom,每一幀都要重新 Layout */
@keyframes popUp {
from { bottom: -180px; }
to { bottom: 20px; }
}
/* 改後:位置固定,改動 translate,只需要 Composite */
@keyframes popUp {
from { translate: 0 200px; }
to { translate: 0 0; }
}
改成 translate 之後,起點和終點的畫面看起來一樣,但每一幀不用再重新排版。用獨立的 translate 還有一個好處:不會和元素原本的 transform 互相覆蓋。
好耶!第 20 篇!!
動畫語法與觀念的基本介紹告一段落,明天突入實作篇。統整一些常見或個人覺得有趣的動畫效果,我們明天見。