Skip to content

主題系統效能優化:動態樣式表注入方法論

概述

本文檔詳細闡述了電商管理平台中,從「JS驅動」的主題切換,演進為「動態樣式表注入」的效能優化方法論。此方法旨在徹底解決主題切換時的延遲問題,實現純 CSS 的極致切換速度。

核心成果

  • 解決延遲:徹底消除主題/模式切換時「慢一拍」的感覺。
  • 極致效能:享受純 CSS 的即時切換速度(<16ms)。
  • 架構優雅:結合了 JS 集中管理的靈活性和 CSS 原生的高效能。
  • 簡化邏輯:移除了複雜的 MutationObserver,簡化了主題切換邏輯。

問題識別:為什麼會「慢一拍」?

根本原因:JS 非同步造成的延遲

在先前的架構中,我們使用 MutationObserver 來監聽 .dark class 的變化。其工作流程如下:

  1. 用戶操作: 點擊切換按鈕,JS 為 <html> 添加 .dark class。
  2. DOM 變化: 瀏覽器更新 DOM。
  3. 非同步通知: MutationObserver (一個非同步 Web API) 偵測到變化,並將回調函式推入任務佇列。
  4. JS 執行: 在下一個事件循環中,JS 執行 applyTheme 函式。
  5. 樣式注入: applyTheme 函式透過迴圈,逐一將 CSS 變數注入為 <html> 的內聯樣式 (inline style)。
  6. 瀏覽器重繪: 瀏覽器根據新的 CSS 變數重新計算樣式並渲染畫面。

這個「JS 繞一圈」的過程,雖然只有幾十毫秒,但足以被人類感知,產生「慢一拍」的延遲感。它永遠無法快過純 CSS 的原生響應速度。

🧠 解決方法論:一次性樣式注入

為了兼顧「JS 集中管理主題」的靈活性和「純 CSS 切換」的極致效能,我們採用了「一次性動態樣式表注入」的方案。

核心原則

  1. 預先編譯 (Pre-compilation):在應用程式初始化時,一次性將所有可能的主題樣式(包含淺色/深色模式)全部生成為 CSS 規則。
  2. 樣式注入 (Stylesheet Injection):將生成的所有 CSS 規則,動態地創建一個 <style> 標籤並注入到網頁的 <head> 中。
  3. Class 切換 (Class Toggling):注入完成後,所有的主題和模式切換,JS 的工作只剩下改變 <html> 上的 class 名稱。後續的樣式變化完全由瀏覽器原生的、最高效的 CSS 引擎處理。

架構對比

特性JS 驅動 (舊方案)樣式表注入 (新方案)改善
切換機制JS 監聽並注入樣式瀏覽器原生 CSS 引擎根本性改變
切換速度非同步,有延遲同步,即時 (<16ms)⭐⭐⭐⭐⭐
效能有 JS 執行開銷零 JS 執行開銷⭐⭐⭐⭐⭐
複雜度需要 MutationObserver移除 MutationObserver✅ 更簡潔
主題管理JS 集中管理JS 集中管理不變

技術實作詳解

1. 時機:應用程式初始化

我們會在 useTheme composable 第一次被實例化時,執行一次性的樣式生成與注入。使用一個旗標 (isStylesheetInjected) 確保此操作只執行一次。

2. 生成 CSS 規則字串

我們會遍歷 themes 物件,為每個主題生成其淺色和深色模式的 CSS 規則。

typescript
// 虛擬碼
function generateAllThemeStyles(themes: Record<string, ThemeConfig>): string {
  let cssString = '';

  for (const themeName in themes) {
    const theme = themes[themeName];
    const themeClassName = `.theme-${themeName}`;

    // 1. 生成淺色模式規則 (e.g., :root.theme-default)
    cssString += `:root${themeClassName} {
`;
    for (const token in theme.tokens) {
      cssString += `  --${token}: ${theme.tokens[token]};
`;
    }
    cssString += '}
';

    // 2. 生成深色模式規則 (e.g., :root.theme-default.dark)
    if (theme.darkTokens) {
      cssString += `:root${themeClassName}.dark {
`;
      for (const token in theme.darkTokens) {
        cssString += `  --${token}: ${theme.darkTokens[token]};
`;
      }
      cssString += '}
';
    }
  }
  return cssString;
}

3. 注入 <style> 標籤

生成完整的 CSS 字串後,使用標準 DOM API 將其注入 <head>

typescript
function injectStylesheet(cssString: string) {
  const styleElement = document.createElement('style');
  styleElement.id = 'dynamic-theme-styles';
  styleElement.textContent = cssString;
  document.head.appendChild(styleElement);
}

4. 重構 useTheme

useTheme 的職責將大幅簡化:

  • applyTheme 函式移除:不再需要逐一設定 CSS 變數。
  • MutationObserver 移除:不再需要監聽 class 變化。
  • setTheme 函式簡化:只負責切換 <html> 上的 theme-* class。
  • useColorModeonChanged:只負責切換 .dark class。
typescript
// 簡化後的 setTheme 邏輯
function setTheme(themeKey: string) {
  const themeName = `theme-${themeKey}`;
  const root = document.documentElement;
  
  // 移除舊的 theme class
  root.className = root.className.replace(/theme-[^�-]+/g, '').trim();
  
  // 添加新的 theme class
  root.classList.add(themeName);
}

預期成果

  • 極致效能:主題和模式切換的延遲感將完全消失,達到原生 CSS 的響應速度。
  • 架構簡化:移除了 MutationObserver 和複雜的 applyTheme 邏輯,使程式碼更易於理解和維護。
  • 關注點分離:JS 負責一次性設定,CSS 負責後續所有切換,職責清晰。
  • 開發體驗:新增主題時,開發者只需在 themes 物件中添加新定義,無需關心其他實作細節。

可複製性

這個方法論不僅適用於主題系統,也適用於任何需要在客戶端根據條件動態切換大量 CSS 變數的場景,例如:

  • 複雜的儀表板佈局切換
  • 基於使用者權限的 UI 視覺調整
  • A/B 測試中的不同視覺方案

本文件記錄了專案從 JS 動態樣式到 CSS 預編譯注入的架構演進,是追求極致效能和優雅架構的最佳實踐。