
當 styled-components 進入維護模式:CSS-in-JS 的過去與未來
這篇整理自 2025 年 5 月在前端讀書會的分享。
2025 年 3 月 18 日,styled-components 的核心維護者 Evan Jacobs 在 Open Collective 上發文,宣布 styled-components 進入維護模式(maintenance mode)。styled-components 曾經是 React 生態系裡最熱門的樣式方案之一,很多專案到現在還在用。這篇從這則消息出發,回頭看 CSS-in-JS 為什麼會出現、後來遇到什麼問題,以及接下來可能往哪裡走。
CSS-in-JS 這十年
styled-components 為什麼進入維護模式
公告裡提到三個原因。
React 的方向改變,破壞了相容性。 React 核心團隊實質上棄用了部分 API,例如 Context API 在 React Server Components(RSC)裡無法使用,也沒有提供遷移路徑。styled-components 的 theming 正是建立在 Context 上。
生態系的風向轉了。 社群整體已經離開 CSS-in-JS 這個概念,轉向 Tailwind 這類 utility-first,或是在 build time 處理樣式的方案。
主要維護者不再在實際產品裡使用它。 從 2018 年左右開始負責 styled-components 的 quantizor(Evan Jacobs)已經沒有在大型專案的正式環境使用 styled-components,對實際使用情境的了解只會越來越少。
對開發者的影響
維護者很早就決定不大幅更動 API,避免既有使用者付出遷移成本。所以像依賴 React Context 這類功能不會被移除,之後仍會有零星的錯誤修復和改善。
不過,維護者也明說:以目前的生態趨勢,不建議在新專案採用 styled-components,或其他大多數 CSS-in-JS 方案。
簡單說,既有專案可以繼續穩定使用,但它不會再有未來。
CSS-in-JS 的興起
從網頁文件到單頁應用
2010 年之後,單頁應用框架陸續出現:AngularJS(2010)、Ember.js(2011)、React(2013)、Vue(2014)。開發重心從靜態的「網頁文件」轉向互動性高的「應用程式」,使用者對網站的期待也跟著提高。
這是前端開發擁抱 JavaScript runtime 能力的時期,CSS-in-JS 就在這個背景下出現。
CSS 在大型專案的七個問題
CSS-in-JS 的概念最早出現在 2014 年。當時在 Facebook 參與 React 開發的工程師 Christopher Chedeau,網路上大家多叫他 Vjeux,在演講〈React: CSS in JS〉裡列出 CSS 在 Facebook 這種規模的專案會遇到的七個問題:
- 全域命名空間:CSS 選擇器都是全域的,任何地方寫的
.button都會影響整個網站。專案一大,名稱就容易撞在一起,只能靠 BEM 這類命名規則讓大家自律。 - 依賴關係:JS 用
import就看得出元件依賴什麼,CSS 沒有這種機制。看一個元件,很難知道它需要哪些樣式;import元件時,對應的 CSS 也不會跟著進來。 - 刪除沒用到的樣式:要刪一條規則時,不知道還有沒有地方在用,結果沒人敢刪,CSS 只增不減。
- 壓縮 class 名稱:JS 的變數可以安全壓縮成
a、b,class 名稱卻同時出現在 CSS 和 JS 的字串裡,工具很難保證兩邊一起改,只好保留原本的長名稱。 - 共享常數:同一個值,例如 header 高度或主色,CSS 和 JS 常常都要用,只能各寫一份,改了一邊容易忘了另一邊。
- 樣式結果不確定:權重相同時,後載入的規則勝出。樣式檔非同步載入時,順序每次可能不同,同一個元素有時套到這個樣式、有時套到那個。
- 隔離:外層可以用
.parent div這類選擇器改到子元件內部的樣式,元件作者沒辦法阻止,一改內部結構,別人的樣式就可能壞掉。
他的解法是把樣式寫成 JS 物件,用 inline style 套到元件上。這樣上面大部分的問題就交給 JS 的模組系統處理:變數有作用域,import 就是依賴關係,沒用到的程式碼可以被移除,常數也能直接共用。
CSS-in-JS 帶來什麼
回頭看,CSS-in-JS 的貢獻主要在三個地方:
- 元件化思維:樣式管理從「整個頁面」縮小到「單一元件」,模組化的能力更強。
- 動態樣式:借用 JavaScript 的語言特性,讓樣式可以依 props 或狀態變化,彈性更高。
- 樣式與邏輯放在一起:結構、行為和樣式寫在同一個檔案,比較好理解、維護,也比較容易搬到別的地方重用。
CSS-in-JS 遇到的問題
Sam Magura 的〈Why We’re Breaking Up with CSS-in-JS〉整理了 runtime CSS-in-JS 的幾個缺點。
Runtime 的效能開銷
元件 render 時,CSS-in-JS 函式庫要把樣式轉成 CSS 再插入 DOM,這個過程會額外消耗 CPU。
更大的問題在瀏覽器端。頻繁插入新的 CSS 規則,瀏覽器每次都要重新計算樣式。在 React 的 concurrent rendering 模式下,渲染可能被中斷、暫停、恢復或取消,重算的成本會更高。React 後來為 CSS-in-JS 函式庫推出 useInsertionEffect,讓樣式在正確的時機插入,但瀏覽器重算樣式的負擔還是在。
Bundle size
使用者要多下載 CSS-in-JS 函式庫本身。函式庫不算大,但還是會讓 bundle 變大。
React DevTools 變得難讀
像 Emotion 這類函式庫會在 React tree 裡插入內部元件,DevTools 的結構變得很亂,debug 比較費力。
和 SSR、RSC 不相容
這是最根本的問題。styled-components 的設計和 Server Components 的運作方式有衝突,而 React 沒有提供 RSC 可以用的 Context 替代方案,元件之間要共享 theme 這類資料就變得很困難。Josh Comeau 在〈CSS in React Server Components〉有詳細的說明。
轉向 zero-runtime 與 server-first
面對這些問題,新一代的 CSS-in-JS 函式庫往兩個方向調整:把樣式處理移到 build time(zero-runtime),以及支援 Server Components。
Linaria
Linaria 在 2017 年由 Callstack 團隊開發,是最早提出 zero-runtime 的方案。它在 build time 把寫在 JS 裡的樣式抽出來,產生靜態的 CSS 檔案,沒有 runtime 開銷。
後來 Linaria 把「把 JS 裡的樣式編譯成 CSS」這段邏輯獨立出來,成為 WyW-in-JS(What you Want in JS),可以搭配不同的 bundler。
幾個遷移案例:
- Airbnb’s Trip to Linaria(2022)
- Dcard 的〈為什麼我們決定放棄 styled-components 並選擇 linaria〉(2022)
從這些案例來看,zero-runtime 對降低 TBT(Total Blocking Time)和 bundle size 都有明顯效果。
Panda CSS
Panda CSS 在 2023 年由 Chakra UI 團隊開源。它在 build time 靜態分析程式碼,再透過 PostCSS plugin 產生 atomic CSS 的 class。
- 支援 RSC
- 支援 PostCSS 的框架都能用
- 內建 design token 的支援
Pigment CSS
Pigment CSS 在 2024 年由 Material UI 團隊開源。它一開始是 Linaria 的 fork,後來改建立在 WyW-in-JS 上,在 build time 把寫在元件旁的樣式轉成 CSS Modules。
- 提供 Next.js 和 Vite 的 plugin
- 支援 RSC
- Material UI v6 開始可以選用 Pigment CSS 作為樣式引擎
Material UI 換過好幾次樣式方案:v4 用 JSS,v5 改用 Emotion。Emotion 和 styled-components 一樣是 runtime CSS-in-JS,theming 也依賴 React.useContext,所以同樣會遇到 RSC 的問題。Pigment CSS 就是為了這個而做的,介紹文章有說明背後的考量。
React 的典範轉移
Next.js 的 CSS-in-JS 文件列出了目前支援 Server Components 的函式庫。React 生態系正在轉變,函式庫要延續下去,就得同時支援 client 和 server 兩種 React。
原生 CSS 追上來了
另一方面,當初需要靠 JavaScript 補足的功能,CSS 自己也慢慢有了。
CSS nesting
CSS 現在可以原生寫巢狀規則,用 & 代表父選擇器,2023 年起成為 Baseline:
.card {
padding: 1rem;
& .title {
font-weight: bold;
}
&:hover {
background: #f5f5f5;
}
}
@media、@supports、@layer、@scope、@container 這些 at-rule 也可以寫在規則裡面:
.card {
padding: 1rem;
@media (width >= 768px) {
padding: 2rem;
}
}
@scope
@scope 可以把樣式限定在特定的元素範圍內,而且不會提高選擇器的權重(specificity):
@scope (.card) {
img {
border-radius: 8px;
}
}
這段只會影響 .card 裡面的 img。分享當時 @scope 還不是所有主流瀏覽器都支援,使用前請先查 MDN 的支援狀況。
或許之後不需要再用 JavaScript 擴充 CSS,CSS 會自己長成我們希望的樣子。
該不該換掉 styled-components
評估是否遷移
維護模式不代表馬上不能用。決定之前可以先問幾個問題:
- 現在有遇到效能瓶頸嗎?TBT 或 hydration 花很多時間嗎?
- 效能優化是目前產品的優先目標嗎?
- 遷移的成本和帶來的好處,哪個比較大?
Josh Comeau 在〈CSS in React Server Components〉也有討論接下來的路怎麼走。
遷移策略
真的要遷移的話,可以參考 Airbnb 的做法:
- 從小範圍開始,用 A/B testing 驗證新方案有沒有效果
- 驗證完,新寫的程式碼都用新方案
- 準備 codemod,批次轉換舊的程式碼
- 搭配截圖測試,確認遷移前後的樣式一致
結語
每個技術的出現,都是為了解決當時背景下的特定問題。CSS-in-JS 在 SPA 興起的年代解決了真實的痛點,後來 React 走向 server、CSS 本身也追上來,它的位置自然跟著改變。
技術和框架都會變,與其擔心錯過什麼,不如專注在眼前要解決的問題,知道自己為什麼做這個選擇。
參考資料
- Thank you,styled-components 維護模式公告
- styled-components entering maintenance mode,Reddit 討論串
- styled-components 進入維護模式,Calpa
- A Brief History of CSS-in-JS: How We Got Here and Where We’re Going,Dan Ward
- React: CSS in JS,Christopher Chedeau
- All You Need To Know About CSS-in-JS,Indrek Lasn
- A Detailed Explanation of CSS-in-JS,Alibaba Cloud
- Why We’re Breaking Up with CSS-in-JS,Sam Magura
- CSS in React Server Components,Josh Comeau
- Airbnb’s Trip to Linaria,Airbnb Engineering
- 為什麼我們決定放棄 styled-components 並選擇 linaria,Dcard
- Linaria
- Panda CSS
- Introducing Pigment CSS,Material UI