Runtime CSS-in-JS vs Zero-runtime CSS-in-JS
新工作上崗的第一份作業是重構舊專案,因為是獨立開發的專案、而且主管想要嘗鮮(?),允許我選會讓自己開心的技術XD
不過考慮到公司大部分同事都用Vue,一開始很猶豫到底是要用React?還是Vue?由於完全不熟Vue的生態,跑去問公司前輩Vue生態圈有沒有類似styled-components的CSS-in-JS框架或套件,結果發現自己對這個名詞不是很熟悉,甚至學了新名詞 ─ zero-runtime CSS-in-JS,所以決定來寫一篇閱讀筆記。
CSS-in-JS
此外,CSS-in-JS不是指特定套件(library)的名稱,而是一堆套件的總稱,都是指「用JavaScript寫CSS樣式」的套件,尤其現在前端很流行採用宣告式(declarative)語法的框架和套件(如React、Vue、Angular、Svelte、等),構成網頁的基本單位變成是一個個組件(components),而CSS-in-JS套件大都用來處理特定組件的樣式。
這篇筆記主要目的不是認識CSS-in-JS的使用方式,如果對CSS-in-JS沒有概念,可以參酌以下兩篇文章:
- 5 things you didn’t know you can do in CSS-in-JS
- An Introduction to CSS-in-JS: Examples, Pros, and Cons
每個CSS-in-JS套件可能會以不同方法解決樣式套用方式和問題,但大方向而言這些套件的主要用途是相同的,都是用來解決傳統CSS的問題,以下會來討論傳統CSS有什麼問題使得有CSS-in-JS套件的出現。
為何有CSS-in-JS
為何會有CSS-in-JS的出現?根據這位作者的文章大致上可歸納為傳統CSS語法有以下問題:
- CSS缺乏模組系統(module system):模組可以封裝、抽象化並獨立(解耦)不同功用的程式碼,也就是使用模組原則上不需要全盤了解模組內的程式碼細節、只需要知道模組的用途,而且可以適時運用模組分離不同程式碼的功能;
- CSS原則上是全域套用:若沒有給予元素特定的屬性(attribute)或類(class)名稱,則所有參照到特定CSS選擇器(selector)的元素都會呈現相同樣式,舉例來說像這樣的
a { color: red; }
type selector沒有特別指名"哪一個"<a></a>
就會套用到所有<a></a>
元素; - 傳統CSS易導致混亂的類名(class name)和規則應用以及冗餘未使用的樣式出現:舉個範例過去要給特定HTML文件元素特定樣式可能會這樣寫
<p is-large-text is-alart has-logged-in ... >...</p>
諸如此類套用一堆CSS選擇器,在寫大型應用程式時,若每個元素都這樣套用CSS樣式,不只很容易忘記元素套用哪些CSS規則(如文字粗斜體、大小、顏色、背景顏色等),大量的CSS樣式也會變得難以管理; - CSS原則上並非依據元素狀態套用樣式:文章一開始提到的宣告式框架和套件大多是用「狀態(state)」來呈現網頁現在的模樣,譬如使用者輸入錯誤密碼會顯示一則輸入密碼錯誤的紅色警告訊息,而「顯示/不顯示」代表這則紅色警告訊息目前的狀態,若使用者輸入錯誤密碼則網頁「顯示」紅色警告訊息,反之可能顯示其他顏色訊息(甚至不顯示任何訊息),而CSS-in-JS其中一項優勢就是元素(警告訊息)可根據狀態(顯示/不顯示)表現出相對應的樣式(紅色)。
(Run-time) CSS-in-JS 缺點
用JavaScript寫CSS樣式免不了會有一些缺點,這裡專門指的是run-time CSS-in-JS的缺點。
以這篇文章談到其中一個CSS-in-JS套件 ─ Emotion 的缺點為例,run-time CSS-in-JS套件在執行時期(runtime)會(1)解析JavaScript成純CSS樣式並注入到HTML文件元素中(這個步驟稱為「serialization」),另外Emotion為了讓樣式只能用於某個組件,還會(2)為組件樣式生成隨機的CSS類別名稱。此外,(3)如果使用者的瀏覽器不允許執行JavaScript檔案自然就看不到需要解析JavaScript才能看到的樣式了。
看得出來CSS-in-JS產生樣式的種種動作會在渲染組件時增加額外的CPU負擔,文章中也以React+Emotion渲染組件的時間數據佐證CSS-in-JS套件確實大大地增加渲染時間;另一篇文章也用一般的 <div>
標籤和styled-components的 styled.div
標籤去證明CSS-in-JS為了解析tagged template耗費多出一倍以上的渲染時間。更甚者,若是碰到頻繁地重新渲染行為,可想而知會加重瀏覽器多少負擔。
儘管使用這類型的CSS-in-JS套件也有一些優化技巧,但如果過度強調優化而對套件的運用限制太多,反而讓這類型的CSS-in-JS套件失去原來的優點。
Zero-runtime CSS-in-JS
前面提到缺點會強調是指runtime CSS-in-JS,那是因為有另一種解析CSS樣式的CSS-in-JS套件是在編譯期(compile time)處理,並編譯成 .css
檔案,自然解決部分上述runtime CSS-in-JS在執行時期解析CSS步驟的缺點,同時加快頁面載入速度。
除了事先編譯這一點不一樣以外,runtime CSS-in-JS有的優點 zero-runtime CSS-in-JS也都有了,而且目前最為知名的zero-runtime CSS-in-JS套件就屬linaria,因為lineria的語法風格跟styled-components大同小異,如果想要嘗試zero-runtime CSS-in-JS,幾乎可以無痛轉去lineria。
另外zero-runtime CSS-in-JS套件比較熱門的有Vanilla extract、語法也是類似styled-components的compiled、bundle size小於1kb的goober;甚至還有可以處理的server-side rendering的Stitches、treat等near-zero-runtime CSS-in-JS套件。
結論&心得
一開始有提到Why We're Breaking Up with CSS-in-JS的文章作者認為純CSS的幾個缺點促使他去使用CSS-in-JS,但最後卻因為CSS-in-JS也有效能上的大缺點導致他放棄CSS-in-JS。
有趣的是,文章底部很多網友留言持反對意見,甚至有網友發表一篇批判文章 ─ Why I never understood CSS-in-JS,他認為「不良的命名、設計架構和指引」,這些習慣才是造成開發者自己寫純CSS會非常混亂的主因,文章作者認為優秀的開發者會很注意自己的樣式設計架構,甚至點出一個許多開發者的通病(當然包含我) ─ 因為沒有良好的開發習慣導致過度仰賴新產物。
設計模式和架構這塊一直是我蠻缺乏的東西,CSS幾個重要的設計模式像是BEM、OOCSS、SMACSS等,也許改天需要好好研究一下🥺
延伸閱讀
- React: CSS in JS techniques comparison
- BEM
- How to Improve CSS Performance
- Using CSS custom properties (variables)
References 5 things you didn’t know you can do in CSS-in-JS An Introduction to CSS-in-JS: Examples, Pros, and Cons What actually is CSS-in-JS? The tradeoffs of CSS-in-JS The Unseen Performance Costs of Modern CSS-in-JS Libraries Why We're Breaking Up with CSS-in-JS Why I never understood CSS-in-JS The unseen performance costs of modern CSS-in-JS libraries in React apps Comparing the top zero-runtime CSS-in-JS libraries Zero runtime CSS-in-JS : Is this where great DX meets top-notch Web Performance? Introduction to Vanilla Extract library. Sweet, isn’t it?