零執行階段 CSS-in-JS:兼顧開發體驗與極致效能
1. CSS-in-JS 的「雙刃劍」
CSS-in-JS 方案,如 styled-components 和 Emotion,透過允許開發者在 JavaScript/TypeScript 檔案中編寫 CSS,徹底改變了元件化開發的樣式管理方式。它帶來了許多好處:
- 元件作用域: 樣式預設與元件綁定,解決了 CSS 全域命名衝突的問題。
- 動態樣式: 可以基於元件的 props 或 state 輕鬆建立動態樣式。
- 程式碼共存: 樣式和元件邏輯放在同一個檔案中,便於維護和組織。
然而,這些便利性並非沒有代價。傳統 CSS-in-JS 函式庫的核心是 執行階段 (Runtime)。當元件在瀏覽器中掛載時,CSS-in-JS 函式庫的 JavaScript 執行階段會:
- 解析範本字串或物件中的 CSS。
- 產生唯一的類別名稱。
- 將樣式動態地注入到文件的
<head>中的<style>標籤裡。
這個過程會增加 JavaScript 的套件體積,並在應用程式啟動和渲染時帶來一定的計算開銷,影響效能。
2. 「零執行階段」的破局之道
為了解決執行階段效能問題,社群提出了一種新的思路:零執行階段 (Zero-Runtime) CSS-in-JS。
其核心思想是:保留 CSS-in-JS 的開發體驗,但在 建置時 (build time) 就完成所有工作。建置工具(通常是一個 Babel 外掛程式或 Vite 外掛程式)會掃描你的程式碼,找到所有 CSS-in-JS 的用法,然後:
- 將 CSS 文字提取出來。
- 產生靜態的
.css檔案。 - 將原始的 CSS-in-JS 程式碼替換為產生的、唯一的類別名稱。
最終,交付給瀏覽器的 JavaScript 套件中 不包含任何 CSS-in-JS 的執行階段函式庫,其效果等同於手寫的高度最佳化的靜態 CSS 檔案。
3. 主流零執行階段方案
a. Linaria
Linaria 是零執行階段領域的先驅之一。它允許你使用熟悉的 styled 標籤範本字串語法。
// YourComponent.js
import { styled } from '@linaria/react';
const Title = styled.h1`
font-size: 2rem;
color: tomato;
`;
// 构建时,这会被转换为:
// <h1 class="Title_a1b2c3d">...</h1>
//
// 并在一个静态 .css 文件中生成:
// .Title_a1b2c3d {
// font-size: 2rem;
// color: tomato;
// }b. vanilla-extract
由 SEEK 公司團隊開發的 vanilla-extract 更進一步,它利用 TypeScript 來創造完全型別安全的樣式。所有樣式定義都是 .css.ts 檔案中的匯出變數,提供了強大的型別推斷和自動補全。
// styles.css.ts
import { style } from '@vanilla-extract/css';
export const title = style({
fontSize: '2rem',
color: 'tomato',
});c. Panda CSS
Panda CSS 是一個較新的競爭者,它借鑑了 Tailwind CSS 的思想,提供了一種「樣式屬性」 (Style Props) 的開發體驗,同時保證了零執行階段的輸出。
import { css } from '../styled-system/css';
function MyComponent() {
return <div className={css({ fontSize: '2rem', color: 'tomato' })} />;
}4. 優勢與權衡
優勢:
- 極致效能: 最終產物是靜態 CSS,沒有 JavaScript 執行階段開銷。
- 更小的套件體積: 無需在客戶端載入 CSS-in-JS 函式庫。
- 強大的開發體驗: 依然享受元件作用域、TypeScript 型別安全和程式碼共存的好處。
權衡:
- 建置時相依性: 必須整合到建置流程中,設定相對複雜一些。
- 動態性受限: 無法根據純執行階段的值(例如,來自 API 的資料)來產生全新的樣式。不過,可以透過 CSS 變數等技術來彌補大部分動態場景。
結論
零執行階段 CSS-in-JS 是對傳統 CSS-in-JS 典範的一次「撥亂反正」。它巧妙地分離了「開發時體驗」和「執行階段效能」,讓我們不再需要在兩者之間做出艱難的妥協。對於追求極致效能和更小打包體積的現代 Web 應用程式來說,它提供了一個近乎完美的解決方案。