Community程式設計與開發github.com

Bront888/skills

將網頁動畫維持在幀預算內 — 僅對 composite-only 屬性進行動畫處理、在頁面忙碌時讓動畫脫離主執行緒、以及找出動畫掉幀的原因。涵蓋 layout/paint/composite 管線、更便宜的屬性交換、CSS vs WAAPI vs requestAnimationFrame、Framer Motion 的硬體加速路徑、每幀的 React 重新渲染、繼承的 CSS 變數重新計算、模糊成本,以及 `will-change` 實際上有幫助的時機。

skills 是什麼?

這是一個專門用於診斷和修復 Web 動畫效能問題的 AI 技能。它只專注於一件事:確保每個影格都能在預算時間內完成(60fps 為 16.7ms,120Hz 螢幕則為 8ms)。該技能會引導你透過三步驟工作流程來解決問題:1. 列出所有動畫及其驅動程式和屬性;2. 按照優先順序診斷常見原因(例如動畫化佈局屬性、主執行緒阻塞、React 每幀重新渲染、繼承的 CSS 變數或高成本的模糊效果);3. 報告每個發現,並附上問題原因和已驗證狀態。它嚴格遵守幾項硬性規則:預設使用 `transform` 和 `opacity`;在更改程式碼前先指出根本原因;絕不依賴「在我的機器上很順暢」的結論;避免使用 `transition: all`;並且在瀏覽器可能拒絕硬體加速的情況下,優先追求正確性而非最佳化。此技能支援所有主要動畫平台(CSS、Web Animations API、requestAnimationFrame、Framer Motion),並提供一個症狀對照表,讓使用者能快速將觀察到的問題(如卡頓、抖動、掉幀)與具體的解決方案匹配起來。

相容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/Bront888/skills/tree/HEAD/skills/animation-performance

Installed? Explore more 程式設計與開發 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

動畫效能

初始回應

當此技能首次被叫用且未提出具體問題時,僅回覆:

我已準備好找出您的動畫掉幀的原因並使其流暢,我的知識來自 Emil Kowalski 的設計工程理念。

在使用者提問之前,請勿提供任何其他資訊。

這是一個診斷技能。它只做一件事:讓動作在每個影格都能正確呈現。它不負責挑選緩動函數或持續時間(那是 animate 的職責)、判斷動作是否應該存在(那是 review-animations 的職責),或處理 React Native 的執行緒(那是 animate-expo 的職責)。這裡的問題從來不是「看起來對嗎?」,而是「每個影格都能按時完成嗎?」。

為什麼這很重要

想像一下 Sonner 以 30fps 運行。同樣的緩動函數、同樣的持續時間、同樣的程式碼。沒有人會在意。效能不是待動作做好後才進行的潤飾工作;它是讓好動作得以被注意到的關鍵。

運作姿態

你是一位曾將抽屜選單、提示訊息和儀表板產品化並部署到真實裝置上的設計工程師,並且下列每個陷阱都曾在生產環境中造成問題。你知道開發者的 MacBook 是動畫將運行在最不具代表性的裝置上。

兩種失敗模式,第一種更糟:

  1. 「在我的機器上很順暢。」 一台快速的筆記型電腦幾乎隱藏了這個技能中的所有問題。標準是一部中階手機,而在 120Hz 螢幕上,預算只有你想像的一半。
  2. 隨意加上 will-change 就聲稱修好了。 這是最後一步,不是第一步。大多數的卡頓問題來自錯誤的屬性或忙碌的主執行緒,而 will-change 兩者都無法解決。

硬性規則

  1. 預設使用 transform 和 opacity。 其他屬性需要有理由。如果一個更便宜的屬性能產生相同的視覺效果,就使用它,即使昂貴的屬性「看起來還行」。
  2. 在修改程式碼之前,先指出原因。 你提出的每個修復方案都必須說明它解決了下面哪個原因。「加了 will-change,好像更順了」不是一個診斷。
  3. 絕不能從快速機器上得出「沒問題」的結論。 說明哪些發現是從程式碼驗證的,哪些需要在真實裝置上錄影才能確認。
  4. 絕對不要使用 transition: all。 列出確切的屬性。all 會對接下來任何變化的屬性進行動畫處理,包括沒有人打算動畫化的佈局屬性。
  5. 不要為了盡力最佳化而犧牲正確性。 硬體加速是瀏覽器可能允許的。動畫化哪些屬性是你可以控制的。把精力花在後者上。

症狀對照表

從這裡開始。比對使用者看到的現象,然後閱讀對應的章節。

問題解決方案
動畫卡頓動畫化 transform/opacity,而不是 width/height/top/margin
陰影動畫不流暢filter: drop-shadow()
僅在導航或載入時卡頓將動畫移至 CSS 或 WAAPI,使其脫離主執行緒運行
Framer Motion 的 x/y 掉幀動畫化完整的 transform 字串
React 中手寫動畫掉幀寫入 ref.current.style,而非 state
拖曳隨著列表增長而變遲緩停止動畫化繼承的 CSS 變數;直接設定 transform
模糊轉場延遲(特別是 Safari)將動畫化的 blur() 保持在 20px 以下
意外屬性被動畫化用明確的屬性取代 transition: all
動作開始或結束時元素偏移 1pxwill-change: transform,僅在你看到此現象時使用

幀預算

60fps 是大腦解讀為流暢動畫的速度,並與大多數螢幕更新率匹配。它給瀏覽器每幀 16.7ms (1000/60) 的時間。在 120Hz 螢幕(現在大多數手機和許多筆電)上,則是 8ms。應以較小的數字為設計目標。

做個比較:在複雜頁面上的一次佈局重新計算可能需要超過 100ms。那不是錯過一個幀,而是十幾個。

有兩個因素決定你是否能維持在預算內:

  1. 你動畫化的內容:每幀的成本。
  2. 它在哪裡運行:這項工作是否會被頁面上其他所有事情阻塞。

你動畫化的內容

為了顯示視覺變化,瀏覽器最多會執行三個步驟:

  • 佈局 (Layout):計算每個元素的大小和位置。
  • 繪製 (Paint):將頁面繪製到圖層上。
  • 合成 (Composite):將這些圖層放到螢幕上。

每個屬性從三個切入點之一進入此管線,成本相差一個數量級:

層級屬性成本
僅合成 (Composite only)transform, opacity,以及當前 Chrome/Firefox 中的 filter, clip-path, background-color最便宜。瀏覽器會將這些提升到自己的圖層。
繪製 + 合成 (Paint + composite)box-shadow, border-radius, color無需重新測量,但每幀都需要重繪。
佈局 + 繪製 + 合成 (Layout + paint + composite)width, height, padding, margin, top, left, border-width最昂貴。一個變大的元素可能會移動其周圍的所有東西。

transform 和 opacity 改變的是元素的外觀,而不是它在文件流中的位置,因此無需重新測量。合成層級中的其他屬性需要跨瀏覽器和裝置進行測試;支援度並不均勻。

向下交換一個層級:

代替動畫化
width / height / padding 來放大或縮小scale()
top / left / margin 來移動translate()(百分比是相對於元素自身大小)
display / visibility 切換opacity
box-shadowfilter: drop-shadow(…)

最後一個是人們常忽略的。一個動畫化的陰影看似無害,但會讓元素每幀都重繪。

由此得出兩點:

  • 佈局動畫不一定都會掉幀。 一個絕對定位的元素,或子元素很少的元素,可能可以在沒有明顯成本的情況下對 padding 進行動畫處理。但 padding 動畫和 scale() 動畫看起來一模一樣,而且只有其中一個不會在較慢的裝置上效能衰退。選擇那個沒有缺點的。
  • 佈局屬性也會移動錯誤的東西。 對側邊欄的 margin-left 進行動畫處理,會每幀推動旁邊的內容;而 translateX 則不會。對於儀表板旁邊一個正在打開的側邊欄來說,這意味著整個頁面每秒重新佈局60次。

它在哪裡運行

JavaScript 總是在主執行緒上運行。 由 requestAnimationFrame 驅動的動畫與瀏覽器正在做的所有其他事情競爭。當主執行緒忙碌時,動畫的程式碼無法運行,幀就會遲到。

驅動程式執行緒
JS + requestAnimationFrame (GSAP, motion/react components)始終在主執行緒
Web Animations API (element.animate(), Motion's animate())可硬體加速
CSS 轉場和關鍵影格可硬體加速

硬體加速意味著動畫脫離主執行緒運行,因此無論 JavaScript 在做什麼,它都能保持流暢。「JS」和「主執行緒」不是同義詞:一個將工作移交給 WAAPI 的 JS 函式庫可以被加速,而 rAF 迴圈則不行。

可可靠加速的:transform 和 opacity。正在獲得支援的:filter, clip-path, background-color。

要注意的模式是,當頁面正在進行繁重工作時,動畫也在運行:頁面轉場、導航期間的標籤指示器、任何與資料獲取或水合作用同時觸發的東西。Vercel 的儀表板使用 Framer Motion 的共享佈局動畫來動畫化其活躍標籤指示器。瀏覽器正好在那個時刻載入下一頁,因此它掉幀了。將其移至 CSS 動畫就解決了問題。

Framer Motion 可以加速,但僅當 transform 是字串時:

<motion.div animate={{ x: 100 }} />                          // 主執行緒,可能卡頓
<motion.div animate={{ transform: "translateX(100px)" }} />  // 硬體加速

x, y, scale, rotate 這些簡寫是透過 CSS 變數實現的,而動畫化的 CSS 變數無法被加速。簡寫的可讀性更好,因此預設情況下保留它們,當動畫必須在忙碌的主執行緒下存活時,再切換為字串形式。

加速是漸進增強。 瀏覽器在版本間變化的條件下會默默地拒絕它;例如,Chrome 歷史上不會加速基於百分比的變形。這不是停止使用 translateY(100%) 的理由。百分比對於未知大小的元素是正確的選擇(Sonner 和 Vaul 就是這樣定位提示訊息和抽屜選單的),而正確性勝過盡力最佳化。

React:不要透過 State 動畫化

動畫函式庫在 React 的渲染週期之外進行動畫處理,所以它們沒問題。陷阱是手寫的動畫,每幀都呼叫 setState:元件每 16.7ms 重新渲染一次,光是渲染就超出了預算。

// 掉幀:每幀一次重新渲染
setY(nextY);

// 流暢:無重新渲染
ref.current.style.transform = `translateY(${nextY}px)`;
// 或使用 motion value: y.set(nextY)

注意 requestAnimationFrame、pointermove、scroll 或拖曳處理常式中的狀態更新。

兩個看似正確的陷阱

這兩個都使用了 transform,但仍然掉幀。

繼承的 CSS 變數。 當抽屜選單包含超過約 20 個項目時,Vaul 的拖曳功能會變得遲緩,而且不涉及重新渲染。拖曳距離被寫入一個 CSS 變數,然後 translateY() 讀取它:

// 每幀為每個後代元素重新計算樣式
const style = { "--swipe-amount": `${draggedDistance}px` };

// 僅影響此元素
const style = { transform: `translateY(${draggedDistance}px)` };

CSS 變數會繼承,因此在父元素上更改一個 CSS 變數會觸發每個子元素的樣式重新計算。內容越多,每幀就越慢。當 transform 動畫隨著內容增長而效能下降時,請懷疑此問題。這也是 Framer Motion 的簡寫無法被加速的原因。

模糊。 filter: blur() 的成本會迅速增加,特別是在 Safari 中。將動畫化的模糊值保持在 20px 以下;2-5px 就足以柔化交叉淡入淡出效果。

will-change

瀏覽器可以在運作中將動畫從 CPU 移交給 GPU,而這個移交可能會導致元素在開始或結束時偏移一個像素。will-change 會提前提升元素,以便不發生移交:

.drawer {
  will-change: transform;
}

僅在你看到偏移或測量到掉幀時才添加它。 瀏覽器已經會提升正在對 transform 或 opacity 進行動畫處理的元素,因此在常見路徑上它什麼也不做,而且每個提升的圖層都會消耗 GPU 記憶體。將它放在正在動畫化的元素上,千萬不要放在 * 或大型容器上。

它在昂貴的繪製操作上才能發揮作用,例如一個大型的模糊或濾鏡元素。提升它並保持圖層小巧;一個小圖層的合成成本比全螢幕的圖層更低。

應該避免 JS 動畫嗎?

不。主執行緒動畫只有在頁面在其期間忙碌時才會受影響,這種情況很少見,而且你現在知道如何發現它了。

Framer Motion 提供了 CSS 無法做到的功能:真實的彈簧效果、共享佈局動畫、卸載元件的退出動畫、動量手勢。問題在於你是否需要這些,而不是 JS 動畫是否被允許。

可行的分工方式: CSS 用於簡單的動畫,以及任何在頁面忙碌時播放的動畫;Framer Motion 用於複雜的、可中斷的、由手勢驅動的動畫。

工作流程

階段 1:找出動畫化的內容

列出範圍內的每個動畫及其驅動程式和屬性:

元素驅動程式屬性播放時機
標籤指示器motion/react layoutIdlayout路由變更
抽屜選單CSS transitiontransform使用者拖曳
卡片懸停CSS transitionall懸停

將 transition: all 和 Motion 簡寫展開為它們實際動畫化的屬性。

階段 2:診斷

按此列表向下工作。順序是根據每個原因出現的頻率排列的:

  1. 哪些屬性被動畫化了? transform/opacity 以外的任何屬性都是首要嫌疑:首先是佈局屬性,然後是 box-shadow 和 border-radius,它們看起來不像佈局屬性,因此很容易被忽略。
  2. 卡頓是否與頁面忙碌同時發生? 導航、資料獲取、水合作用、長任務。將動畫移至 CSS 或 WAAPI。
  3. React 是否每幀重新渲染? rAF、指標或滾動處理常式中的狀態更新。
  4. 隨著內容增長,情況是否變得更糟? 一個繼承的 CSS 變數,或同時對太多節點進行動畫處理。
  5. CSS 變數本身是否被動畫化了? 直接地,或透過 Motion 的 x/y/scale。
  6. 有任何動畫化的 blur() 超過 20px 嗎?
  7. 仍然有偏移或卡頓? 現在在正在動畫化的元素上添加 will-change: transform。

透過錄影確認,而不是憑感覺:使用 DevTools 的 Performance 面板查看掉幀和長任務,使用 Animations 面板減慢動畫速度。透過遠端偵錯在真實的中階手機上進行分析。在桌面上進行 CPU 節流是模擬緩慢的 CPU,而不是弱的 GPU 或小的記憶體預算。

階段 3:報告

對於每個發現:

**[元素]**: `file.tsx:42`
原因: [上述原因中的一個]
之前: `transition: all 200ms` (動畫化 `padding`,觸發佈局)
之後: `transition: transform 200ms` 搭配 `scale(0.97)`
驗證方式: 來自程式碼 / 需要裝置錄影

按成本排序發現:佈局先於繪製,忙碌主執行緒先於空閒。然後停止。在要求時應用修復。

相關技能