Skip to content

Sass 效能與產出品質:讓編譯結果不要失控

Sass 的效能與產出品質建議

Sass 的效能問題幾乎不在編譯速度,Dart Sass 編譯幾千行只要幾百毫秒。問題全在產出的那份 CSS。

原因很單純:Sass 寫起來太省力。一個迴圈三行,產出三百條規則;巢狀縮排看起來很整齊,攤平後選取器有五段。你在原始碼看到的份量,與實際產出的份量嚴重脫節。

坑一:巢狀過深

scss
.page .sidebar .menu .item .link { color: #333; }

三個代價:優先權 高到難以覆蓋、檔案帶著重複的祖先路徑、樣式綁死 DOM 結構。

對策: 巢狀不超過三層,用 BEM 讓選取器維持一層。用 Stylelint 的 max-nesting-depth 自動把關,見 source map 與 Stylelint

坑二:@extend 的選取器爆炸

scss
.btn { }
.card .btn { }
.modal .btn:hover { }

.btn-primary { @extend .btn; }   // 一次繼承,三處合併

繼承鏈一長,選取器數量成長得很快,而且產出的順序不由你決定。

對策:@extend %placeholder,不要繼承真實類別;不確定就 改用 混入。理由見 @extend

坑三:迴圈的組合爆炸

scss
// 20 類 × 8 個值 × 4 個斷點 = 640 條
@each $prop in $properties {
  @each $val in $values {
    @each $bp in $breakpoints { }
  }
}

而專案裡真正用到的可能不到一成。

對策: 只產生會用到的組合、把變體做成可關閉的開關、或改用會按需產生的原子化框架。見 工具類別

坑四:abstracts 裡誤放 CSS 規則

scss
// abstracts/_mixins.scss
.debug-outline { outline: 1px solid red; }   // ❌

模組系統 保證只編譯一次,所以不會像 @import 時代那樣重複輸出十份,但這條規則的輸出位置會變得無法控制。

對策: abstracts 只放定義。見 abstracts 該放什麼

混入的重複不必擔心

css
.a { display: flex; align-items: center; }
.b { display: flex; align-items: center; }

看起來浪費,但這種重複字串正是 gzip 最擅長壓縮的東西,實際傳輸量增加有限。

優化的順序

  1. 刪掉沒用到的規則 ← 這裡有最大的收穫
  2. 減少產生的組合數
  3. 開啟壓縮與 gzip
  4. 合併重複的媒體查詢 ← 收益最小

多數人跳過第一步直接做第四步,方向錯了。

壓縮

bash
npx sass src/main.scss dist/main.css --style=compressed --no-source-map

Vite 與 Webpack 在正式建置時預設就會壓縮。

壓縮不會幫你刪東西

compressed 只去掉空白與換行,重複的規則、沒用到的類別它全部保留。

移除未使用的樣式

js
// postcss.config.js
module.exports = {
  plugins: [
    require("@fullhuman/postcss-purgecss")({
      content: ["./src/**/*.html", "./src/**/*.vue", "./src/**/*.js"],
      safelist: [/^is-/, /^has-/],   // JavaScript 動態加的類別要保住
    }),
  ],
};

safelist 一定要設

由 JavaScript 動態加上的類別(is-activehas-error)不會出現在靜態掃描裡,沒列進白名單就會被誤刪。

想知道有多少沒用到:Chrome DevTools 的 Coverage 面板,實際操作一遍網站,它會標出每個檔案的未使用比例。

合併媒體查詢

@media 寫在元件裡會讓同一個斷點在產出裡出現幾十次。真的想合併:

js
require("postcss-sort-media-queries")()

不要因此改變寫法

「媒體查詢寫在元件裡」的維護價值遠高於那點體積。要合併就交給工具,不要退回把所有斷點樣式集中在檔案尾端的舊做法。見 響應式 mixin

編譯真的變慢時

症狀檢查
每次存檔都要好幾秒是不是還在用 node-sass?換成 sass
只改一個元件卻全部重編建置工具的 HMR 設定
產出檔案異常大上面那四個坑
記憶體吃很兇有沒有巢狀三層以上的迴圈

一份檢查清單

  • [ ] 巢狀不超過三層
  • [ ] 沒有 @extend 真實類別
  • [ ] abstracts 裡沒有任何 CSS 規則
  • [ ] 迴圈產生的組合都真的用得到
  • [ ] 沒有用 node-sass
  • [ ] 沒有殘留的 @debug
  • [ ] 正式建置有壓縮
  • [ ] 用 Coverage 看過未使用比例

CSS 本身的效能主題見 CSS 效能優化

常見問題

Sass 專案的 CSS 為什麼特別容易肥大?

因為寫起來太省力。一個迴圈就能產生幾百條規則,巢狀讓選取器不知不覺變長,你在原始碼裡看到的份量與實際產出的份量嚴重脫節。

混入的重複輸出需要優化嗎?

通常不需要。重複的宣告字串正是 gzip 最擅長壓縮的東西,實際傳輸增加有限。真正該優化的是那些從來沒被用到的規則。

重複的媒體查詢要合併嗎?

壓縮後影響很小,通常不必特別處理。真的想合併可以在編譯後接一個 PostCSS 外掛,但不要為此改變把媒體查詢寫在元件裡的習慣,那個維護價值更高。

怎麼知道產出的 CSS 有多少沒用到?

用 Chrome DevTools 的 Coverage 面板實際跑一遍網站,它會標示每個檔案有多少比例未被使用。比例太高就該考慮接上移除未使用樣式的工具。

延伸閱讀