Styling Linear for the future with StyleX
作者 Kenneth Skovhus。發表日 2026-08-26。來源是 Linear 官方 blog。 Linear 用 1,000 多個 PR,把 React 應用程式從 styled-components 遷移到 StyleX。
本篇用 80% STE 寫法整理(見 ASD-STE100):一句一事、用詞固定、句型簡單。 固定用詞:建置期=build time、渲染期=render time、樣式契約=styling contract、隔空改樣式=styling at a distance、逃生門=escape hatch。
統整
1. 離開 styled-components 的理由
styled-components 服務 Linear 很長一段時間。它有三個好處:
- API 很有彈性。
- 樣式和元件放在一起。
- 需要特殊效果時,可以直接寫原生 CSS。
後來彈性變成負債。作者列出四個理由:
- 隔空改樣式太容易。 CSS 本來就容易隔空改樣式。
styled(Button)這種寫法讓「從外面打開元件來改」變成常態。元件因此不必講清楚自己的樣式契約。 - 改版後的回歸很難收尾。 Linear 幾個月前改版。改版後團隊一直在追一條很長的 UI 回歸尾巴。
- 團隊和 agent 都需要邊界。 團隊變大。agent 對 codebase 的貢獻也越來越多。所以 UI 組合的邊界必須定義清楚。
- 效能。 CSS-in-JS 在渲染期產生樣式,並注入 CSS 規則。升級 React 18(concurrent rendering)之後,團隊看到效能退化。同時 styled-components 進入維護模式。這是最後一根稻草。
2. 選型條件
團隊先列出五個不可讓步的條件:
- 樣式生成大部分要在建置期完成,不要在渲染期做。
有什麼原因是要讓其在建置期完成?(答在最後的「讀者提問與回答」)
- 要讓隔空改樣式刻意變難。只靠慣例勸退不算數。
- 元件要能對外明確暴露樣式契約。
- 跨檔案的樣式合併要可預測。不可以玩 specificity 遊戲。
- 函式庫要和 React 合得來、要持續維護、要適合當長期賭注。
團隊另外看兩種人體工學:
- 開發者人體工學:常見情境要簡單,樣式要貼近元件,需要時要有務實的逃生門。
- agent 人體工學:這一項的重要性越來越高。
怎麼評估 agent 人體工學(答在最後的「讀者提問與回答」)
3. 為什麼選 StyleX
團隊看過 React 生態大部分的樣式方案。
vanilla-extract 是最接近的替代品:
- 優點:有靜態抽取,有強型別。
- 缺點:API 比較破碎。元件檔和樣式檔分離,這一點不符合團隊的工作方式。
StyleX(Meta) 的平衡比較好:
- 樣式和元件放在同一個檔案。
- API 很小。
- 樣式解析是決定性的。
- 提供型別安全的樣式契約。
- 刻意讓外部很難伸手進元件改樣式。
這些限制有代價。從 CSS template literal 換到受限的 atomic 系統之後,三種模式變難:
- parent-dependent selector
- global selector
- 透過 wrapper 重新改樣式
作者說這一點正是重點:最難遷移的那些模式,就是讓舊系統在規模下難以推理的元兇。
4. 怎麼讓大遷移變得可行
開工時,styled-components 很難自動化。原因有三個:
- 它給你一個圖靈完備的語言。
- 它給你完整的 CSS 能力。
- 它的 API 完全開放。
所以同一個意圖有無數種寫法。當時 agent 很容易產出「看起來對但其實不對」的東西。
Linear 沒有正式的 design system,這讓事情更難。團隊有共用元件。但是很多元件長出來的是寬泛的 API,不是清楚的樣式契約。
因此作者從決定性的 codemod 開始。這個工具叫 styled-components-to-stylex-codemod。它的起點是一大包測試和一套刻意設計的架構。agent 之後才幫忙擴建它。它包含三樣東西:
- 線上 playground
- 跨檔案的 selector 處理
- 大部分已知 edge case 的回歸覆蓋
作者補充一句:如果今天才開始,團隊會更大幅度地直接交給 Fable 和 Sol 執行。
5. 過渡期的三個降險做法
團隊不想凍結產品開發。所以兩套樣式系統必須共存一段時間。團隊用三個做法降低風險:
- 先定義樣式地基。 codemod 做有用的事之前,團隊要先定好 StyleX 的 variables、constants 和共用 primitives。
- 派給 agent 的範圍要窄。 agent 在五個條件下表現比較好:範圍窄、有 codemod runner、有範例、有驗證腳本、有清楚的 checklist。團隊也試過讓 agent 看 render 出來的 UI,並比對生成的 CSS 和原版。但是 agent 經常卡在驗證視覺差異,例如複雜的 hover 狀態、theming 分支、細微的 layout 差異。所以在 Fable 和 Sol 推出之前,團隊需要大量人工測試。
- 從葉節點開始。 團隊先遷移不包裝、也不改其他 styled-components 樣式的元件。這樣做有三個好處:diff 比較小、和 cascade 的互動比較少、可以先在低風險的地方學,之後再碰共用 primitive。
6. 怎麼推動團隊採用
團隊要讓進度可見。他們做了四件事:
- 在 dev tool bar 加一個計數器。它顯示目前頁面上還有多少 styled-components。你在 Linear 裡導覽時,數字會上下變動。
- 做一個工具標示元件用的是哪一套。它讓人一眼看出某一塊是否遷移完。它也成為大家動手遷移的誘因。
- 持續產生進度圖。團隊用它估算何時能跟 styled-components 道別。完成時團隊在 Zoom 上喝酒慶祝。
- 讓一個 bot 在 PR 裡點名新增的 styled-components。它讓大家互相負責。它也讓回歸更難溜進來。
進度數據(React 樣式檔案的佔比):
| 時間 | styled-components | StyleX |
|---|---|---|
| 2026-03-08 | 100% | 0% |
| 2026-05-31 | 56% | 44% |
| 2026-06-07 | 49% | 51% |
| 2026-08-06 | 0% | 100% |
每週新增的 styled-components 檔案數:3 月每週 12 到 22 個。5 月 31 日第一次歸零。6 月之後幾乎都是 0。
7. 遷移後怎麼守住慣例
團隊用兩層工具強制慣例:
- Oxlint 做快速的本地檢查。它檢查常見的寫法錯誤、design token 的使用,以及 React props 蓋掉 StyleX 產出的情況。
- 型別感知的 repository checker 做跨檔案檢查。有些規則需要的脈絡超過單檔 linter。這個工具跨檔案追蹤元件、import 和樣式。它驗證樣式 API。它也偵測 StyleX 的 property-level 合併靜默移除 base 樣式或 conditional 樣式的情況。
規則分成五類:
- 遷移與遺留清理。 禁止 import
styled-components。找出仍然對外暴露、但沒有用到的className和styleprops。 - 元件樣式 API 與跨檔案傳遞。 目標是讓
sxprop 成為元件之間的標準樣式介面。規則檢查四件事:StyleX 元件有暴露sx、sx在本地樣式之後才合併、元件不會默默丟掉sx、元件不會宣告了卻從不套用樣式 props。repository checker 另外追蹤 wrapper 和 polymorphic 元件的 StyleX 支援、確認sx正確 materialize、偵測跨元件邊界消失的 conditional 樣式。 - 樣式組合與優先序。 規則抓
className、style、JSX spread、sx、stylex.props()、mergedSx()互相覆蓋的情況。規則也抓 StyleX 特有的合併問題,例如呼叫端的一個扁平 override 會換掉元件的所有 conditional state。shorthand 規則屬於同一類:它拒絕多值的 margin 和 padding shorthand,也拒絕會讓 StyleX property priority 默默改變結果的 shorthand 與 longhand 條件組合。 - 設計與互動一致性。 規則把寫死的視覺值換成共用變數,包含顏色、字體、陰影、動畫、游標、細邊框。規則也標準化 hover、press 和 link 行為。例如
highlightMixin在指標裝置映射到:hover,在觸控裝置映射到:active。外部連結的底線用同一個共用 mixin,這樣它才能相容使用者的 link-underline 偏好。團隊允許 suppression,但是 suppression 的範圍必須窄,而且必須有理由。 - 編譯器、runtime 與 theme 安全。 規則拒絕對
defineConsts的值做算術。規則對未定義的 dynamic style 值設防。規則把裸的 CSS variable 換成型別化的defineVars()參照。規則也禁止把 themed token 當成 root-leveldefineVars()的預設值。在目前 DOM subtree 之外 render 時,規則要求使用 themed portal 元件。
團隊另外為 Vite 加了幾個建置期擴充。它們處理 StyleX 不直接支援的行為,例如跨指標裝置和觸控裝置的一致 hover 與 press 回饋。
8. StyleX 不是封閉系統
團隊沒有消滅所有逃生門。他們用兩種方式留出口:
用 CSS Modules 當 fallback。 有些問題本質上還是 CSS 形狀的,例如 global selector、改第三方 DOM 的樣式。CSS Modules 在這裡是有 scope、而且明示的 fallback。
擴充 StyleX 做 theming。 Linear 的 theme 是生成的,不是手寫的:
- 輸入是幾個參數:base color、accent color、contrast。
- 系統在 LCH 空間推導出一百多個顏色變數。
- 選取一列時,系統不是換一個背景色。它用被選取的背景當新的 base,重新生成整個 theme。label、border 和控制項因此全部重新推導。
- focus、menu、elevated surface 也比照辦理。每一個都透過巢狀
ThemeProviderscope 到自己的 subtree。
這個模型和 StyleX 內建的 theming 不太合。團隊的做法是:
- 元件照常對普通的
stylex.defineVars()token 寫樣式。 - 團隊把這些 token 當成佔位符。
ThemeProvider取出 StyleX 生成的 hashed custom-property 名稱。ThemeProvider在 runtime 為每個生成的 theme 注入一條 CSS 規則。這條規則 scope 在 provider 邊界上。
結果是:theming 模型維持不變,其餘的樣式系統移出渲染路徑。
9. 成果
端到端的影響很難歸因。原因是它和團隊持續不斷的效能優化交織在一起。
為了隔離樣式的成本,團隊用 Fable 比對 3 月的 profile 和今天的 client。他們只看產生樣式和注入樣式相關的工作。結果有三個數字:
- 在 view-heavy 的頁面,拿掉渲染期的樣式注入減少主執行緒 CPU 工作約 20% 到 35%。
- 在團隊的測試裡,中階機器上大約快 30%。
- client 從一次導覽注入數百條 CSS 規則,變成 0 條。
作者認為更有價值的不是效能,是樣式有了清楚的邊界:
- 元件有明確的樣式契約。
- 優先序是決定性的。
- 很多以前靠慣例或 cascade 的模式,現在由工具強制。
結論句:團隊得到一套在 runtime 更快、更難被誤用、也更適合「越來越多由 agent 寫的 codebase」的樣式系統。
團隊有用現成的 UI component 嗎? 沒有遇到什麼問題嗎?(答在最後的「讀者提問與回答」)
反覆出現的關鍵字
作者一直回到這幾個詞。這是文章自己的重點,不是我挑的:
- styling contract(樣式契約)/boundaries:元件要明確宣告自己能被怎麼改。這是全文的軸。
- styling at a distance(隔空改樣式):具體反例是
styled(Button)。 - build time vs render time(建置期 vs 渲染期):整個效能論證建立在這組對比上。
- deterministic(決定性):同一個詞用在三個層次,分別是樣式解析、codemod、優先序。
- merge / precedence / property priority(合併、優先序):
sx、mergedSx()、stylex.props()、shorthand 和 longhand。lint 規則最大的一塊在這裡。 - agent ergonomics:這個概念出現在四個地方,分別是動機、選型條件、遷移執行、結論。
- enforced by tooling, not convention(用工具強制,不靠慣例):lint、repository checker、PR bot 都是這句話的實作。
- escape hatch(逃生門):CSS Modules、Vite 擴充、自製 theming。
- leaf nodes / scope(葉節點、範圍):遷移順序和 agent 派工用同一個概念,就是縮小範圍。
矛盾點
文章內部
- 「彈性是負債」和「我們保留逃生門」互相拉扯。 離開 styled-components 的理由是彈性造成隔空改樣式。但是團隊保留 CSS Modules 處理 global selector 和第三方 DOM。他們也自己擴充 StyleX 做 theming。差別應該在逃生門是否明示、是否有 scope。但是文章只說「我們沒有試圖消滅逃生門」。它沒有給判斷界線。
- agent 的角色前後反差。 前面說 styled-components 太開放,agent 很容易產出看起來對但其實不對的東西,而且 agent 卡在驗證視覺差異。後面說如果今天開始會更依賴 Fable 和 Sol 直接做。文章把差別歸因於模型能力提升。但是當初讓 agent 失手的結構性原因是「同一個意圖有無數種寫法」,這個原因並沒有消失。
- 三個效能數字量的不是同一件事。 20% 到 35% 是主執行緒 CPU 工作,而且限於 view-heavy 頁面。30% 是中階機器上的測試結果。作者自己先說過端到端影響很難歸因。引用時要帶著各自的範圍。
- 沒有 design system 被列為困難,但遷移也沒有建立 design system。 團隊建立的是樣式地基和 lint 規則。(推論) 這兩者的差別值得想一下:用工具強制的契約,是不是就等於 design system 的功能性替代品。
與 vault 既有筆記
- 和 規格檢查要機械化進 CI,不能靠紀律 完全同向。本文的 lint 規則、repository checker 和 PR bot,是那條主張在樣式領域的完整實作。本文可以當它的案例。
- 和 把 codebase 想成一棵樹:trunk 與 leaf 的分界是有沒有被隔開 呼應。那篇講 review 深度由 blast radius 決定。本文的「從葉節點開始遷移」是同一個判準,但用在改動順序上。
- 和 監督 AI 產出的檢查成本可能比自己寫還高 一致。agent 卡在驗證複雜 hover、theming 分支、細微 layout 差異,就是「檢查成本高到抵銷產出」的具體場景。本文給的解法是把驗證做成可執行的腳本和 checklist。
- 和 Review 要用沒生成過這段 code 的 agent 可以對照。這裡的驗證者不是另一個 agent。驗證者是 codemod 的測試套件和型別感知的 checker。做法是把驗證從模型判斷換成機械判斷。
讀者提問與回答
這一段是讀者在讀統整檔時留下的問題,以及對照原文的回答。標
(推論)的部分原文沒有寫。
Q1. 為什麼要讓樣式生成在建置期完成?
文章給的理由有四個:
- CSS-in-JS 在渲染期做兩件事:產生樣式、注入 CSS 規則。這兩件事都是額外的工作。
- 這些工作落在主執行緒上。它和 React 的渲染搶同一份時間。
- 升級 React 18 之後,團隊看到效能退化。React 18 引入 concurrent rendering。styled-components 當時已進入維護模式。
- 遷移後的量測支持這個理由。在 view-heavy 頁面,拿掉渲染期的樣式注入減少主執行緒 CPU 工作約 20% 到 35%。client 從一次導覽注入數百條 CSS 規則變成 0 條。
補充 (推論):
- 建置期生成的樣式是一個靜態 CSS 檔。瀏覽器可以快取它。瀏覽器也可以和 JS 並行下載它。渲染期生成的樣式沒有這兩個好處。
- 能在建置期算完,代表樣式不依賴執行期的任意值。所以「移到建置期」同時買到效能和可預測性。文章的選型條件第 1 條和第 4 條指向同一個限制,但是文章沒有把這兩件事連起來說。
Q2. 怎麼評估 agent 人體工學?
文章沒有給評估方法。 它只說這一項越來越重要。這是一個洞。
文章有的是反例。它說 styled-components 難自動化,原因有三個:圖靈完備的語言、完整的 CSS 能力、完全開放的 API。結果是同一個意圖有無數種寫法。agent 因此容易產出看起來對但其實不對的東西。
從這個反例反推判準 (推論):
- 一個意圖有幾種寫法? 寫法越少越好。
- 錯誤會不會當場被抓到? agent 寫錯之後,要有機械回饋告訴它錯在哪。
- 驗證要不要靠眼睛? 需要用眼睛判斷的地方,agent 人體工學就差。文章說 agent 經常卡在複雜 hover 狀態、theming 分支、細微 layout 差異。
- 結果可不可以不執行畫面就比對? 決定性的樣式解析讓同一份輸入得到同一份輸出。這讓 diff 可以當驗收標準。
文章實際的評估方式是間接的 (推論):它用「agent 在哪裡卡住」當訊號,然後把卡住的地方做成工具。它沒有事前打分的量表。
Q3. 團隊有用現成的 UI component 嗎?有遇到什麼問題嗎?
文章沒有直接說。 它沒有點名任何第三方 UI 函式庫。
兩條間接證據:
- 逃生門那一段說,global selector 和 restyling third-party DOM 這類問題仍然是 CSS 形狀的,所以用 CSS Modules 處理。要改第三方 DOM 的樣式,代表頁面上有不是團隊寫的 DOM。
- 文章說 Linear 沒有正式的 design system。團隊有共用元件,但是很多元件長出來的是寬泛的 API。這句話講的是內部元件,不是外部函式庫。
能確認的是:第三方 DOM 是一個已知的麻煩,而且解法是退回 CSS Modules,不是用 StyleX 解。他們用了哪些現成元件、各自踩到什麼,文章沒有寫。
抽出的原子筆記
「隔空改樣式要靠工具變難」與「從葉節點開始遷移」沒有獨立成篇:前者併進樣式契約那篇,後者留在本檔的第 5 節。
樣式系統(CSS MOC):
- CSS-in-JS 的成本是渲染期產生樣式與注入規則
- 能在建置期算完,代表樣式不依賴執行期的任意值 — 讀者提問 Q1 延伸出的主張
- 樣式契約要用工具強制,逃生門要明示且有 scope — 兩層檢查 + 五類規則 + 逃生門,合併自原本的兩則候選
- 把 token 當佔位符,由 ThemeProvider 在邊界注入一條規則
遷移執行(重構與技術債 MOC):
待釐清(AI MOC):
- 怎麼評估一個 API 的 agent 人體工學? — 讀者提問 Q2,文章沒給評估方法