零執行階段 CSS-in-JS:兼顧開發體驗與極致效能

5 分鐘

1. CSS-in-JS 的「雙刃劍」

CSS-in-JS 方案,如 styled-componentsEmotion,透過允許開發者在 JavaScript/TypeScript 檔案中編寫 CSS,徹底改變了元件化開發的樣式管理方式。它帶來了許多好處:

  • 元件作用域: 樣式預設與元件綁定,解決了 CSS 全域命名衝突的問題。
  • 動態樣式: 可以基於元件的 props 或 state 輕鬆建立動態樣式。
  • 程式碼共存: 樣式和元件邏輯放在同一個檔案中,便於維護和組織。

然而,這些便利性並非沒有代價。傳統 CSS-in-JS 函式庫的核心是 執行階段 (Runtime)。當元件在瀏覽器中掛載時,CSS-in-JS 函式庫的 JavaScript 執行階段會:

  1. 解析範本字串或物件中的 CSS。
  2. 產生唯一的類別名稱。
  3. 將樣式動態地注入到文件的 <head> 中的 <style> 標籤裡。

這個過程會增加 JavaScript 的套件體積,並在應用程式啟動和渲染時帶來一定的計算開銷,影響效能。

2. 「零執行階段」的破局之道

為了解決執行階段效能問題,社群提出了一種新的思路:零執行階段 (Zero-Runtime) CSS-in-JS

其核心思想是:保留 CSS-in-JS 的開發體驗,但在 建置時 (build time) 就完成所有工作。建置工具(通常是一個 Babel 外掛程式或 Vite 外掛程式)會掃描你的程式碼,找到所有 CSS-in-JS 的用法,然後:

  1. 將 CSS 文字提取出來。
  2. 產生靜態的 .css 檔案。
  3. 將原始的 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 應用程式來說,它提供了一個近乎完美的解決方案。