Mike

19 Nov, 2025

CSS Transition 動畫效能提升與方法的選擇!

今天這個篇文章主要想分享一下我在工作上遇到的一個問題與最後的解決方案的選擇。

問題起因

在開發側邊選單時,我遇到一個有趣的效能問題,當點擊按鈕改變選單的 width 時,transition 動畫會明顯卡頓。表面上看起來是個簡單的寬度變化,但實際上因為粗心所以第一時間沒發現子元件隱藏著嚴重的效能問題。
.sidebar {
  width: 240px;
  transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}

.sidebar.collapsed {
  width: 80px;
}

根本原因:複雜的 Gradient 計算

問題不只在於 width 本身,但主要根本問題是我的選單內有一個較為複雜的 divider 元件。
.divider {
  height: 10px;
  margin: 0.5rem 0;
  background-image: linear-gradient(
    to right,
    transparent calc(50% - 150px),
    #d3d3d3 calc(50% - 150px),
    #d3d3d3 calc(50% + 150px),
    transparent calc(50% + 150px)
  );
  background-size: 100% 1px;
  background-position: center;
  background-repeat: no-repeat;
}
因爲這個東西讓我的 transition 動畫卡頓,找了好久才發現是這個東西造成的!

寬度改變了時候發生了什麼事?

當選單 width 從 240px 變成 80px 時:

  • 選單寬度改變 (240px → 80px)
  • 瀏覽器開始重排:
       1. 選單的新寬度計算
       2. divider 的 calc(50% - 150px) 需要重新計算!
       3. calc(50% + 150px) 也要重新計算!
       4. 檢查這個改變是否影響其他元素
       5. 檢查兄弟元素是否受影響
       6. 檢查父元素是否需要重新排版
  • 動畫執行 60 幀
       1. 上面的計算重複 60 次
       2. 大量重排和重繪
       3. 卡頓!

calc() 的計算問題,一不注意就踩到!

calc() 表達式需要在動畫的每一幀重新計算:

  • calc(50% - 150px) 其中 50% 是基於寬度的百分比
  • width 改變了 → 50% 的值改變了 → 整個表達式需要重新計算
  • 60 幀 × 複雜計算 = 效能大幅下滑

 

很多人可能會問為什麼不用 transform 來處理?

在深入介紹我的解決方案之前,我想先解釋為什麼在這個案例中,為什麼我選擇不是效能本來就好的 transform 而是針對 width 去處理!

最多人會用的方案之一:用 transform: scaleX() 縮放

有人可能會說:「為什麼不用 transform 避免重排?」

關於瀏覽器的選染跟重新繪製我有寫一篇文章專門介紹,有興趣可以先看看
https://thecodingpro.com/b/1pXFBuxCnFuTUIhHoo8O

.sidebar {
  width: 240px;
  transform: scaleX(1);
  transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}

.sidebar.collapsed {
  transform: scaleX(0.333);
}

表面上看起來不錯,效能跟體驗都挺棒的,因為 transform 只涉及繪製,不涉及重排

但問題來了!!! 子元件也會被縮放!

選單寬度 240px → 使用 scaleX(0.333)

  1. Menu items 也被縮放到 1/3
  2. divider 也被縮放到 1/3
  3. 文字也被縮放,變得很小
  4. padding/margin 都被縮放了

要解決這個,你需要在每個子元件上反向縮放:

接下來會衍生出一些可能的情況:
  • 每添加一個新的子元件,就要加反向縮放
  • 多層嵌套時,計算變得複雜(scale(3) 不等於 scale(1/0.333))
  • CSS 重複,容易出錯
  • 假設 6個月後,同事看到這堆 transform,他的表情會很奇妙!
.menu-item {
  transform: scaleX(3);  /* 反向縮放來抵消 */
  transform-origin: left;
}

.divider {
  transform: scaleX(3);  /* 每個子元件都要寫 */
  transform-origin: left;
}

/* 如果有更多子組件... 每個都要加 */

 

所以我最終的選擇的解決方案 width + transition + contain

.sidebar {
  width: 240px;
  transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);

  /* 添加這一行就好 */
  contain: layout style paint; 
}

.sidebar.collapsed {
  width: 80px;
}

/* 子元件完全不用改 */
.menu-item {
  padding: 0.75rem;
  cursor: pointer;
  border-radius: 0.375rem;
}

.divider {
  height: 10px;
  margin: 0.5rem 0;
  background-image: linear-gradient(
    to right,
    transparent calc(50% - 150px),
    #d3d3d3 calc(50% - 150px),
    #d3d3d3 calc(50% + 150px),
    transparent calc(50% + 150px)
  );
  background-size: 100% 1px;
  background-position: center;
  background-repeat: no-repeat;
}

先來聊聊 contain 是什麼?

MDN 是這個說的:

 

contain 標示了元素及其內容盡可能獨立於文檔樹的其餘部分。限制使 DOM 的一部分得以被隔離,且透過將佈局、樣式、繪製、尺寸或其任意組合的計算限制於 DOM 子樹而非整個頁面使效能受益。限制也可用於限制 CSS 計數器和引號的作用域。

 
剩下的大家再參考一下 MDN 的文件,我就不贅述了~ 
當你設定了 contain: layout style paint 的時候,等於就是在告訴瀏覽器說「 這個元件內部的改變不會影響外面。只在這個盒子裡面處理就好。

contain 三個值的含義

  1. layout - 隔離排版
    • 這個元素內部的排版改變不影響外部 divider 的位置變化只在選單內部計算
  2. style - 隔離樣式
    • CSS 計數器和自訂屬性改變不向外傳播
  3. paint - 隔離繪製
    • 選單的重繪不需要重繪整個頁面計算 gradient 時也只影響選單區域

contain: layout style paint;
那這樣重排的流程就會變成:

效能對比

指標 沒有 contain 有 contain 提升
幀率 20-40%
divider calc() 重新計算 影響整個頁面 只影響選單區域 隔離化
瀏覽器需要檢查 整個頁面 layout 只檢查選單 大幅減少
使用者體驗 明顯卡頓 流暢無感 體驗極好
 

那何時該用 contain?

特別適合的場景:

  • 複雜的動畫元件( width、height 動畫 )
  • 包含複雜計算的子元件( gradient 計算、複雜 selector )
  • 獨立的組件( 側邊選單、卡片 )
  • 滾動區域( 有 overflow 的容器 )

不需要的場景:

  • 簡單元件(沒有子元件、沒有複雜計算)
  • 會影響頁面佈局的元件

重點整理

  1. 問題根源:calc() gradient 計算需要動態重新評估
  2. Cascading Effect:width 改變 → calc() 重新計算 → 瀏覽器檢查整個頁面 layout
  3. contain 的力量:一行 CSS 就能隔離這個級聯效應
  4. 無副作用:維持現有的動態效果,不會破壞任何功能
  5. 效能提升:60-75% 的計算量減少

延伸思考

如果你的網站有其他動畫卡頓的問題,先問自己:

  • 這個元素是否包含複雜的 CSS 計算?( 如 calc()、複雜 gradient )
  • 這個元素的改變是否可能影響頁面其他部分
  • 是否可以用 contain 隔離它

往往只需要一行 CSS,就能帶來巨大的改善!


最後的建議:效能優化不一定很複雜。有時候簡單的一行 contain: layout style paint 就能帶來 60% 以上的動畫效能提升。當遇到動畫卡頓,先試試看 contain 吧!