iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0
Modern Web

從實務需求出發:前端視覺與 UI 互動開發實踐系列 第 20 篇

【Day 20】動起來:網頁效能,卡卡 OUT!

  • 分享至 

  • xImage
  •  

不難發現,前幾天只提到用 transform、opacity、translate、rotate 等來寫動畫。當然 CSS 其他屬性也能做成動態效果,但若寫過多、過重會影響網頁效能,使用起來明顯會有卡頓的感覺。

今天,來研究動畫對網頁效能的影響:先來理解瀏覽器如何渲染畫面?為什麼有的 CSS 屬性輕、有的 CSS 屬性重?再到動畫寫得重會有什麼影響,最後再回頭看之前踩過哪些坑。

那我們開始吧!


一、瀏覽器如何渲染畫面?

瀏覽器要把畫面顯示出來,會依序走過四個步驟:

  1. Style(樣式計算):把 CSS 規則套用到元素上,算出每個元素最終的樣式(也就是 DOM + CSSOM = Render Tree,詳細可以參考文章)。
  2. Layout(排版):算出每個元素的大小與位置。常被提到的「回流(reflow)」指的就是這一步。
  3. Paint(繪製):把元素畫成一個個像素。常被提到的「重繪(repaint)」就是這一步。
  4. Composite(合成):把畫好的各個圖層疊起來,呈現在畫面上。

做動畫的時候,瀏覽器每一幀都要重新走一遍流程,而且是從「需要更新的那一步」開始,往後全部重跑。越前面的步驟被觸發,後面要做的事就越多。

依照動畫改了什麼屬性,成本大致分三種:

動了什麼 觸發的步驟 例子 成本
影響大小或位置 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 只是把已經畫好的圖層,換個位置或透明度再疊一次,輕量許多。


二、動畫寫得重會怎樣?

主執行緒與合成器執行緒

先來了解瀏覽器裡有兩條跟畫面有關的主要執行緒:

  • 主執行緒(Main Thread):執行 JavaScript、解析 HTML/CSS、計算樣式、決定元素位置的 Layout 以及進行 Paint。
  • 合成器執行緒 (Compositor Thread):負責合成,也就是把圖層疊起來、處理圖層的位移與透明度變化,獨立於主執行緒運作。這讓主執行緒忙著執行 JavaScript 時,合成器執行緒仍可處理部分捲動與動畫,確保畫面流暢。

如果動畫是用 transform 和 opacity 屬性,就能在合成器執行緒處理。若只涉及 Reflow 或 Repaint,就得回到主執行緒處理,若主執行緒一忙,那麼動畫就跟著卡頓。

可能導致的問題

  • 掉幀、畫面卡頓:以 60fps 來說,每一幀大約只有 16.7ms 的時間,要跑完樣式計算、排版、繪製、合成這一整套流程。若超時這一幀會被跳過,網頁就會看起來一頓一頓的。
  • 主執行緒被占住,連帶影響互動:樣式計算、排版、繪製和 JavaScript 都在同一條「主執行緒」上。動畫如果一直觸發排版或繪製,使用者點按鈕時,可能要等一下才有反應。
  • 增加記憶體占用與耗電:後面會提到,為了讓動畫變順而額外建立的「Composite Layer」,每一層都要吃記憶體,而持續 Reflow 或 Repaint 也會增加 CPU 負載,增加耗電量。

實際比較

要讓方塊往右移動 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 連結 操作看看,嘗試做出以下操作,得到的反饋分別是:

  1. 卡住主執行緒:按下按鈕後,用一段 JavaScript 占滿主執行緒 3 秒。可以看到方塊 A 停止動作;方塊 B 不受影響。
  2. 讓排版變貴:多次按下「加 3000 個節點」讓頁面節點暴增。可以看到方塊 A 出現卡頓;方塊 B 一樣不受影響。

三、回顧踩過的坑

回頭檢查以前專案裡的寫法,有三個地方正好踩到前面說的雷。

濫用 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 篇!!
動畫語法與觀念的基本介紹告一段落,明天突入實作篇。統整一些常見或個人覺得有趣的動畫效果,我們明天見。


參考資料


上一篇
【Day 19】動起來:transition、animation 與 @keyframes
下一篇
【Day 21】動畫實作:掀起釘選的紙片
系列文
從實務需求出發:前端視覺與 UI 互動開發實踐 共 22 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言