跳轉到

5.4 動畫與渲染效能#

瀏覽器的渲染管線:動畫為何會卡#

一切效能問題都要回到管線:

步驟 觸發條件 成本
Layout 改 width/height/top/font-size 重排整棵樹,最貴
Paint 改 background/color/box-shadow 重繪區域
Composite 改 transform/opacity 只動 GPU,幾乎免費
\[ \text{60 FPS 的預算} = 16.67 \text{ ms/幀} \quad(1000 \div 60) \]

動畫卡頓的定義:某幀的計算超過 16.67ms。最佳化 = 把工作挪到管線的便宜段。

黃金法則:只動 transform 與 opacity#

/* ❌ 觸發 Layout:每幀重算整頁版面 */
.box { transition: width 0.3s; }
.box:hover { width: 300px; }

/* ✅ 只觸發 Composite */
.box { transition: transform 0.3s; }
.box:hover { transform: scaleX(1.5); }   /* 視覺上變寬,版面不變 */

/* ✅ 進出場用 opacity + transform 取代 display */
.modal {
  opacity: 0;
  transform: translateY(8px) scale(0.98);
  transition: opacity 0.2s, transform 0.2s;
}
.modal.open { opacity: 1; transform: none; }

位移用 transform: translate() 而非 left/top —— 因為 transform 發生在合成階段(且可預先提升圖層)。

強制同步版面(layout thrashing):JS 的經典陷阱#

// ❌ 讀寫交錯 → 每圈都強制立刻重排
items.forEach((el) => {
  const h = el.offsetHeight;      // 讀 → 強制 layout
  el.style.height = h + 10 + 'px'; // 寫 → 標記 dirty
  // 下一圈讀取時被迫立刻重排 → N 次重排!
});

// ✅ 先全部讀,再全部寫(批次化)
const heights = items.map((el) => el.offsetHeight);  // 讀 N 次(1 次重排)
items.forEach((el, i) => (el.style.height = heights[i] + 10 + 'px')); // 寫 N 次(1 次重排)
\[ T_{\text{交錯}} = N \times T_{\text{重排}} \quad\quad T_{\text{批次}} = 1 \times T_{\text{重排}} \]

讀寫分離是 DOM 效能的第一實務紀律。

CSS 動畫 vs JS 動畫#

/* CSS:合成器執行,JS 阻塞時依然順暢 */
@keyframes slide {
  from { transform: translateX(0); }
  to   { transform: translateX(100px); }
}
.banner { animation: slide 0.5s ease-out; }
CSS animation/transition JS requestAnimationFrame
執行者 合成器執行緒(主執行緒忙也不卡) 主執行緒
控制 簡單(播放/暫停/事件) 完全控制每幀
適用 進出場、懸浮、載入 遊戲、物理、資料驅動
// rAF:與螢幕同步的 JS 動畫(取代 setTimeout)
function frame(t) {
  box.style.transform = `translateX(${Math.min(t / 5, 100)}px)`;
  if (t < 500) requestAnimationFrame(frame);
}
requestAnimationFrame(frame);

平滑度之外的細節#

will-change:圖層提示(勿濫用)#

.slider { will-change: transform; }  /* 提前建立圖層,避免動畫首幀頓挫 */
/* 過度使用 = 記憶體爆炸:每個圖層都是一張位圖 */

動效的功能性:過場即回饋#

動畫的價值不是炫技,而是解釋因果:元素從哪來、到哪去、什麼狀態變了。材料設計的「變形移動」與 macOS 的縮放最小化都在做同一件事 —— 用運動維持物件恆常性,降低認知負荷。

時長的經驗值:微互動 100–200ms、進出場 200–300ms、大型轉場 400ms 內。超過 500ms 的 UI 動畫 = 使用者開始不耐煩。

效能量測#

  • DevTools → Performance 面板:錄製滾動/動畫,看紫色(recalculate style)、綠色(layout)條
  • Rendering 面板開啟 Paint flashing(重繪區域高亮)與 FPS meter
  • 目標:動畫期間綠色 layout 條應為零

習題#

  1. 把「手風琴展開(用 height)」改寫為只觸發 Composite 的版本(提示:max-height 的問題?grid-template-rows 0fr→1fr?)。
  2. 示範 layout thrashing:寫 20 個元素的交錯讀寫,用 Performance 面板量測,再批次化對比。
  3. 解釋為什麼 left 動畫在手機上比 transform 卡很多,從管線與圖層兩個角度。