效能最佳化:壓小 CSS 與加速建置 | TailwindCSS 完整教學

2026/09/07
效能最佳化:壓小 CSS 與加速建置 | TailwindCSS 完整教學

效能最佳化 是把 TailwindCSS 專案從「能跑」推向「跑得又快又輕」的最後一哩路。在 v4,全新的 Oxide 引擎讓增量建構快到以微秒計,天生的 tree-shaking 搭配自動內容偵測,讓 production CSS 通常只有幾 KB。這一篇帶你把效能拆成三個面向——建構速度CSS 檔案大小執行期渲染,學會用 @source 精準掃描、量測產出大小、避開 safelist 膨脹,讓網站不只寫得快,也跑得快。

前言

效能最佳化(Performance Optimization) 在 TailwindCSS 的語境裡,講的不是單一件事,而是三件常被混為一談的事:你建置專案要等多久(建構速度)、產出的 CSS 檔案有多大(輸出大小)、以及使用者瀏覽器跑動畫、渲染畫面順不順(執行期效能)。把這三件事分清楚,你才知道遇到「慢」的時候該從哪裡下手。

先用一個生活化的類比。把 Tailwind 的效能想成經營一間出貨的工廠。「建構速度」是你的產線把成品組裝出來要多久——換上 v4 的 Oxide 引擎,等於把老舊的手工產線換成自動化機械臂,原本要等好幾秒的批次,現在幾乎是一按就好。「CSS 檔案大小」是你打包出貨的箱子有多重——Tailwind 天生只把「客人真的下單的商品」裝箱(只產生用到的 utility),而不是把整個倉庫塞進去,所以箱子輕巧。「執行期渲染」則是客人收到箱子、拆開使用時順不順——這取決於你用的動畫屬性會不會逼瀏覽器重排整個版面。三者環環相扣,但優化手法各不相同。

本系列以 TailwindCSS v4 為預設版本。本篇你將學到:

  • 三個效能面向:建構速度(Oxide 引擎)、CSS 產出大小(tree-shaking)、執行期渲染(動畫屬性),以及各自的優化策略
  • 精準掃描:v4 自動內容偵測的運作原理,以及何時該用 @source / @source not 手動校正掃描範圍
  • 量測產出:如何實際量測 production CSS 的大小、把它納入建置流程與 CI/CD 監控
  • 避開膨脹:safelist 過寬、掃描過廣、動態拼接 class 等常見坑,以及對應的正確做法

本系列以 TailwindCSS v4 為預設版本,範例皆以 v4 語法示範。凡涉及 v3 差異之處,我會特別標註。

核心概念

三個效能面向:先分清你在優化什麼

「Tailwind 效能」這個詞太籠統。實務上你會碰到的效能問題,幾乎都能歸到下面三格之一。把它們攤開對照,你就知道每種「慢」該用哪種藥:

面向它衡量什麼主要影響因素v4 的優化來源
建構速度建置/重建要等多久掃描範圍大小、引擎實作Oxide(Rust)+ 增量快取
CSS 產出大小輸出檔案多少 KB用到的 class 種類數、safelist天生 JIT + tree-shaking
執行期渲染瀏覽器跑得順不順動畫觸發的渲染階段選對 transform/opacity

這張表的重點是:三者的優化手法幾乎不重疊。建構慢,你要看的是掃描範圍與引擎;CSS 太大,你要看的是「用了多少種不同的 class」與 safelist;畫面卡頓,你要看的是動畫用了哪些 CSS 屬性。搞錯方向,就會發生「拚命縮 CSS 卻解決不了動畫卡頓」這種白工。本篇主軸放在建構速度CSS 產出大小這兩個 Tailwind 最直接掌控的面向,執行期渲染我們會點到重點屬性。

建構速度:Oxide 為什麼能快到微秒級

v4 建構速度的躍升,核心是換了引擎。Oxide 是 v4 用 Rust 重寫的全新引擎,取代 v3 的純 Node.js 實作。它快在三個關鍵設計上。

第一,多執行緒平行掃描。 Oxide 用 Rust 的並行能力同時掃描你專案裡的原始檔,找出被用到的 class 候選,而不是像 v3 那樣單執行緒逐一處理。第二,增量建置快取。 這是「微秒級」的真正秘密——Oxide 記住每個檔案上次用了哪些 class,當你存檔重建時,若這次沒有出現任何新的 class,它就直接沿用上次的結果,幾乎不做重算。第三,內建 Lightning CSS 負責解析、加 vendor prefix 與壓縮,取代 v3 需要外掛的 PostCSS 工具鏈。

官方基準測試給出的量級大致是:完整建置(full build)最高快約 5 倍;無新增 class 的增量重建,以微秒計,可快超過 100 倍。 你可以這樣理解這兩個數字——第一次建置(冷啟動)因為要從頭掃描全部檔案,提升是「數倍」級別;但開發時你 99% 的重建都是「改了個顏色、調了個間距」,並沒有引入從沒用過的新 class,這種情況 Oxide 直接命中快取,壓到微秒等級,快到你存檔幾乎是即時看到畫面更新。

要吃滿這份速度紅利,你基本上不用做任何特別設定——用《建置工具整合》介紹的 @tailwindcss/vite@tailwindcss/postcss 接上專案,Oxide 就自動生效。你唯一能幫倒忙的,是讓它去掃描過廣、過多的目錄(例如整個 node_modules),那會拖慢掃描階段。所以「維持掃描範圍精準」不只影響 CSS 大小,也直接影響建構速度。

CSS 產出大小:tree-shaking 是天生的,不是後製的

很多從其他 CSS 框架轉來的人會有個誤解:以為 Tailwind 是「先產生一大包 CSS,再刪掉沒用到的」。這個理解是反的。 Tailwind 的真相是——它天生只產生你用到的 utility,壓根不存在「先大後刪」這回事。

運作邏輯是這樣:Tailwind 掃描你的原始檔,把出現過的 class 名稱蒐集成一份「候選清單」,然後只為清單上的 class 生成 CSS 規則。你寫了 bg-cyan-600,它就生成一條 bg-cyan-600 的規則;你從沒寫過 bg-lime-300,那條規則就永遠不存在於輸出裡。而且同一個 class 不管在專案裡出現一次還是一千次,都只生成一條規則。所以最終 CSS 的大小,由一個變數決定:你用了多少「種」不同的 class,而不是你用了多少「次」,更不是 Tailwind 總共提供了多少種。

這帶出幾個影響輸出大小的因素,值得記在心裡:

  • class 種類數是主因:用越多種不同的 utility(含各種顏色、間距、字級),輸出越大。但因為是線性累加、且只算「種類」,一般專案很難把它撐大。
  • variant 組合會各自佔一條:hover:bg-cyan-700bg-cyan-700 是兩條規則;sm: md: lg: 各前綴也各自獨立生成。這是必要成本,不是浪費。
  • 自訂設定只擴大「可用範圍」,不直接增重:你在 @theme 加了新顏色,只是讓更多 class 變成「可用」,但沒用到的一樣不會生成。

因為這套機制,多數 Tailwind 專案的 production CSS 壓縮後只有幾 KB 到數十 KB,不需要任何傳統意義上的「purge 步驟」。

v3 差異:v3 沒有自動內容偵測,你必須在 tailwind.config.js 手動維護 content: [...] 路徑陣列,並依賴建置時的 purge 機制刪去未使用的 class。設定漏了、或忘了在 production 開啟 purge,就可能把整套龐大的開發版 CSS 部署上線。v4 靠 Oxide 自動偵測,預設就是「只產生用到的」這個最佳狀態。

自動內容偵測與 @source:讓掃描既全又準

v4 的 CSS 大小之所以能自動維持最小,靠的是自動內容偵測(automatic content detection)。Oxide 會自動找到專案裡的模板檔(.html.jsx.tsx.vue.svelte 等)、自動尊重 .gitignore(略過 node_modules、建置產物等)、並自動忽略二進位檔(如圖片)。這意味著多數專案完全不用設定掃描路徑,就能得到「全掃到、且不掃多餘」的理想結果。

但有兩種情況需要 @source 這個「掃描校正閥」出手:

@import "tailwindcss";

/* 情況一:把預設掃描範圍外的來源納入(例如第三方 UI 套件的 class) */
@source "../node_modules/@my-ui/components";

/* 情況二:反向排除會拖慢或誤判的路徑(例如舊版 legacy 目錄) */
@source not "../legacy";

@source 對效能有雙重意義。往「加」的方向,它確保你真的用到的第三方 class 不會因為在預設範圍外而被漏掉(避免「樣式缺一塊」)。往「減」的方向,@source not 讓你剔除不該掃、掃了只會拖慢建構或誤納入無用 class 的路徑。掌握這一加一減,你就能把掃描範圍調到「不多不少」——這對建構速度與 CSS 大小是雙贏。

v3 差異:v3 的 content 陣列是「從零開始、必須列滿」的心態,漏列就掃不到;v4 的 @source 是「預設全自動、例外才補」的心態。這是根本的設計轉向。

實作範例

理論看完,把量測與優化實際做一次。目標很單純:知道自己的 CSS 有多大、確認掃描範圍精準、產出壓縮過的 production 檔。

範例一:用 @source 精準控制掃描範圍

先確認掃描範圍。多數專案什麼都不用寫,自動偵測就夠;需要校正時,在 CSS 入口用 @source 一加一減:

/* src/main.css:v4 的 CSS 入口 */
@import "tailwindcss";

/* 加:把某個確定會用到的第三方 UI 套件納入掃描 */
@source "../node_modules/@headlessui/react/dist";

/* 減:排除一個放了大量無關檔案、掃了只會拖慢的目錄 */
@source not "./src/legacy-vendor";

判斷要不要動 @source,有兩個訊號。該加:你用了某個第三方元件庫的 class,但畫面上那些樣式沒生效——很可能該庫在 node_modules 裡,超出預設掃描範圍,需要明確納入。該減:你的專案有個又大又雜的目錄(舊碼、產生器輸出),掃它既拖慢建構又可能誤收無用 class,用 @source not 剔除。沒有這兩個訊號,就別動它——自動偵測已經是最佳解。

/* v3 對照:同樣的意圖,v3 寫在 tailwind.config.js */
/* module.exports = {
     content: [
       './src/**/*.{html,js,ts,jsx,tsx}',
       './node_modules/@headlessui/react/**/*.js',
     ],
   } */

範例二:量測 production CSS 的實際大小

「感覺很小」不算數,量出來才算。把量測寫進 package.json 的 scripts,產出時順手看大小:

{
  "scripts": {
    "build:css": "tailwindcss -i ./src/main.css -o ./dist/output.css --minify",
    "size": "tailwindcss -i ./src/main.css -o ./dist/output.css --minify && gzip -c ./dist/output.css | wc -c"
  }
}

重點是 --minify 這個旗標:它讓內建的 Lightning CSS 壓縮輸出,production 一律要加。跑完之後,用最基本的指令看檔案大小,並看「gzip 後」的大小——因為使用者的瀏覽器實際下載的是 gzip(或 brotli)壓縮過的版本,那才是真正影響載入速度的數字:

# 產出壓縮後的 production CSS
npm run build:css

# 看原始檔案大小(未 gzip)
ls -lh dist/output.css

# 看 gzip 後的大小——這才是使用者實際下載的量級
gzip -c dist/output.css | wc -c

一般專案這個 gzip 後的數字會落在幾 KB 到數十 KB。如果你看到它意外地大(例如上百 KB),那幾乎可以斷定是掃描範圍太廣、或 safelist 開太寬,把用不到的 class 也生成了——這就是下一節要防的坑。

範例三:把 CSS 大小納入 CI/CD 監控

手動量測容易忘,更穩的做法是設一道「大小上限」的關卡,讓 CI 在 CSS 超標時直接讓建置失敗,逼你回頭檢查。這能及早攔住「不小心引入的膨脹」:

{
  "scripts": {
    "test:size": "bundlesize"
  },
  "bundlesize": [
    {
      "path": "./dist/output.css",
      "maxSize": "25 kB",
      "compression": "gzip"
    }
  ]
}

這段設定的意思是:dist/output.css 經 gzip 後不得超過 25 KB,否則 test:size 失敗。把它接進 CI/CD pipeline,任何讓 CSS 暴增的改動(例如某人手滑加了個過寬的 safelist)都會被當場擋下。把「CSS 大小」變成一個會回報的指標,而不是靠人記得去看,是大型專案維持長期輕量的關鍵。上限數字依專案規模設定,小型站抓 10 到 15 KB、中大型站抓 25 到 50 KB 都是合理起點。

範例四:執行期——動畫只碰 transformopacity

最後補上執行期這一格。瀏覽器的渲染管線是 Style → Layout → Paint → Composite,越後面的階段成本越低。只觸發 Composite 的屬性效能最好——就是 transformopacity;而動畫 widthheightmargintop 這類會觸發 Layout(重排整個版面),最傷效能:

<!-- ✅ 高效能:hover 上移只用 transform,只觸發 Composite -->
<div class="transition-transform duration-200 hover:-translate-y-1">
  卡片懸停上移
</div>

<!-- ✅ 淡入淡出只用 opacity,同樣高效 -->
<div class="opacity-0 transition-opacity duration-300 hover:opacity-100">
  漸顯效果
</div>

<!-- ❌ 低效能:動畫 width 會觸發 Layout,逐格重排版面 -->
<div class="w-0 transition-all duration-200 hover:w-48">
  避免動畫 width
</div>

<!-- ✅ 改用 scale 取代 width 動畫,效能天差地別 -->
<div class="scale-x-0 origin-left transition-transform duration-200 hover:scale-x-100">
  用 scale 取代 width
</div>

心法一句話:要動的東西,盡量用 transform(平移、縮放、旋轉)和 opacity 來表達,少直接動幾何屬性。 這跟 CSS 大小、建構速度是獨立的一格,但同樣是「效能最佳化」的一環,別漏了。

常見錯誤與最佳實踐

坑一:safelist 開太寬,把 CSS 撐爆

最常見的 CSS 膨脹來源。safelist 的用途是「強制保留某些掃描不到的 class」(通常因為 class 由後端或 CMS 動態決定),但很多人貪方便用萬用 pattern,一次生成成百上千條用不到的規則,把幾 KB 的 CSS 撐大好幾倍:

/* ❌ v4:用 @source inline() 塞進過寬的組合,生成一堆用不到的 class */
@source inline("bg-{red,orange,amber,yellow,lime,green,teal,cyan,blue,indigo,violet,purple,pink}-{50,100,200,300,400,500,600,700,800,900,950}");
// ❌ v3:同樣的錯誤心態,pattern 用萬用符
module.exports = {
  safelist: [{ pattern: /bg-.+/ }],  // 生成所有背景色組合,災難
}

正確做法:優先用「完整字串對照表」,讓 Tailwind 靜態掃得到,根本不需要 safelist。 把可能的 class 完整寫在程式碼裡:

// ✅ 完整字串對照表:每個 class 都是靜態可掃描的完整字串
const bgMap = {
  red:   'bg-red-600',
  blue:  'bg-blue-600',
  green: 'bg-green-600',
}
// 使用:element.className = bgMap[color]  ← Tailwind 掃得到,無需 safelist

真的非用 safelist 不可時(class 完全由執行期決定),把範圍收到最窄,只列確定會用到的組合,別用 .+:

/* ✅ v4:pattern 收窄到只列確定會用的顏色與深淺,而非全部 */
@source inline("{bg,text}-{red,blue,green}-{100,500,900}");

坑二:掃描範圍過廣,建構變慢又可能收進垃圾

第二常見的坑,是把 @source(或 v3 的 content)指向過大的目錄,最典型就是掃整個 node_modules。這同時傷兩件事:掃描階段變慢(建構效能),而且可能把第三方套件裡碰巧長得像 class 的字串誤收進來(CSS 大小):

/* ❌ 把整個 node_modules 塞進掃描 → 建構變慢 + 誤收無用 class */
@source "../node_modules";
// ❌ v3 的等價錯誤
module.exports = { content: ['./node_modules/**/*.js'] }

正確做法:只精準指向你真正用到 class 的那個子目錄,而不是它的上層:

/* ✅ 只掃真正用到的那個套件的 dist 目錄 */
@source "../node_modules/@my-ui/components/dist";

記住:自動偵測預設已經尊重 .gitignore 並略過 node_modules,所以你只在「確實要納入某個被忽略的路徑」時才手動加 @source,而且要指到最窄。

坑三:動態拼接 class 字串,掃不到就沒樣式

這是 tree-shaking 機制的頭號天敵。Tailwind 只能做靜態分析——它讀原始碼、找完整的 class 字串。任何用字串串接或模板變數「拼」出來的 class,它都看不到,那些 class 就不會被生成,樣式神秘消失:

// ❌ 動態拼接:Tailwind 掃不到,這些 class 不會生成
const cls = 'bg-' + color + '-500'          // 字串串接
const cls2 = `text-${size}`                 // 模板字串動態部分

正確做法:永遠寫完整的 class 字串,用對照表或條件式來選,而不是用拼的:

// ✅ 條件式選完整字串
const cls = isActive ? 'bg-blue-600' : 'bg-gray-200'

// ✅ 對照表存完整字串(同坑一的解法)
const sizeMap = { sm: 'text-sm', lg: 'text-lg' }
const cls2 = sizeMap[size]

這個坑跟 safelist 常被混淆:動態拼接的正解幾乎都是「改寫成完整字串」,而不是「用 safelist 硬補」。safelist 只留給「連完整字串都寫不出來、class 純由執行期外部資料決定」的極少數情況。

坑四:v4 還在手動維護 content,或忘了 --minify

兩個殘留自 v3 習慣的小坑。其一,升級 v4 後還到處找 content: [...] 要填——v4 已自動偵測,多數專案不用碰,硬填反而容易列錯漏掉。其二,production 建置忘了加 --minify,輸出的 CSS 沒壓縮,平白多出好幾 KB 的空白與換行:

# ❌ production 卻沒壓縮,檔案平白變大
tailwindcss -i ./src/main.css -o ./dist/output.css

# ✅ production 一律加 --minify,交給 Lightning CSS 壓縮
tailwindcss -i ./src/main.css -o ./dist/output.css --minify

順帶一提 CDN 版本:Play CDN(<script src="https://cdn.tailwindcss.com">)會在瀏覽器執行時掃描 DOM 即時生成樣式,沒有建置期 tree-shaking,整包載入且執行期有額外開銷,效能不適合正式環境,只當原型工具用。正式站一律走建置途徑產出壓縮過的靜態 CSS。

最佳實踐小結

  • 信任自動偵測與天生 tree-shaking:v4 預設只產生用到的 utility,production CSS 通常幾 KB,別自己畫蛇添足。
  • 掃描範圍求「不多不少」:@source 加真正用到的外部來源,@source not 剔除拖慢的路徑,絕不掃整個 node_modules
  • safelist 是最後手段:優先用完整字串對照表讓靜態掃描抓得到;非用不可時把 pattern 收到最窄。
  • 動態 class 改寫成完整字串:用對照表或條件式,別用串接或模板變數拼 class。
  • 量測並監控 CSS 大小:production 加 --minify,把 gzip 後大小納入 CI/CD 的 bundlesize 上限,防止悄悄膨脹。
  • 動畫只碰 transformopacity:避免動畫幾何屬性觸發 Layout 重排。

小結

這是 TailwindCSS 完整教學 系列的第三十八篇。上一篇《建置工具整合》,我們把 Tailwind 接進了真實專案——認識了 Oxide 引擎與 Lightning CSS,搞懂三種安裝途徑;這一篇,我們順著同一條線深入效能,學會怎麼讓接好的 Tailwind 跑得又快又輕。回顧幾個重點:

  • 三個效能面向:建構速度、CSS 產出大小、執行期渲染,各有各的優化手法,別搞錯方向。
  • Oxide 建構速度:Rust 引擎 + 增量快取,完整建置最高快約 5 倍,無新 class 的增量重建以微秒計、可快超過 100 倍,你幾乎不用設定。
  • 天生 tree-shaking:Tailwind 只產生用到的 utility,CSS 大小取決於「class 種類數」而非用量,production 通常只有幾 KB。
  • 精準掃描:自動內容偵測尊重 .gitignore,例外才用 @source 一加一減校正範圍。
  • 避開膨脹:safelist 過寬、掃描過廣、動態拼接 class 是三大坑,正解多半是「完整字串 + 收窄範圍」。
  • v3 差異:v3 需手動 content + 建置時 purge 才能瘦身;v4 靠 Oxide 自動偵測,預設就是最佳狀態。

效能調校讓 Tailwind 專案兼顧開發速度與上線體積,是工程化不可少的一環。下一篇《Next.js 整合》,我們把鏡頭對準當今最主流的 React 框架——看 TailwindCSS v4 如何接進 Next.js App Router,處理 Server Components、Turbopack 與 postcss.config 的實務細節,讓你在真實的全端專案裡把 Tailwind 用得順手。

BenZ Software Developer

熱愛技術的軟體開發者,在這裡分享程式開發經驗與學習筆記。

本週主打

AI 自動化入門包

你每天手動在做的那些煩事,其實 AI 可以自己跑。這份給你 10 個照著做就會的自動化工作流 + 50 個複製即用的提示詞,不用會寫程式。

看看這個產品 →