hello 大家好,我是 superZidan,這篇文章想跟大家聊聊 為什么我們正在放棄 CSS-in-JS
這篇文章將深入的挖掘我當(dāng)時(shí)為什么會(huì)在項(xiàng)目中使用 CSS-in-JS (本文使用 Emotion 方案 ),而現(xiàn)在為什么正在放棄這樣的方案。
什么是 CSS-in-JS
CSS-in-JS 允許你直接使用 JavaScript 或者 TypeScript 修改你的 React 組件的樣式
import styled from '@emotion/styled'
const ErrorMessageRed = styled.div`
color: red;
font-weight: bold;
`;
function App() {
return (
<div>
<ErrorMessageRed>
hello ErrorMessageRed !!
</ErrorMessageRed>
</div>
);
}
export default App;
styled-components 和 Emotion 是 React 社區(qū)最流行的 CSS-in-JS 方案。本文中我只是提及到 Emotion ,但是我相信大部分的使用場(chǎng)景也同樣適用于 styled-components。
本文專(zhuān)注于 運(yùn)行時(shí)類(lèi)型的 CSS-in-JS ,styled-components 和 Emotion 都屬于這個(gè)類(lèi)型。因?yàn)?CSS-in-JS 還有另一種類(lèi)型,編譯時(shí)類(lèi)型 CSS-in-JS 這塊會(huì)在文章末段稍微提及到。
CSS-in-JS 的優(yōu)缺點(diǎn)
在我們深入了解 CSS-in-JS 的模式和它對(duì)性能的影響之前,我們先從總體的了解一下為什么我們會(huì)使用這項(xiàng)技術(shù)以及為什么要逐步放棄
優(yōu)點(diǎn)
1.Locally-scoped styles: 當(dāng)我們?cè)诼銓?xiě) CSS 的時(shí)候,很容易就污染到其他我們意想不到的組件。比如我們寫(xiě)了一個(gè)列表,每一行的需要加一個(gè)內(nèi)邊距和邊框的樣式。我們可能會(huì)寫(xiě)這樣的 CSS 代碼
.row {
padding: 0.5rem;
border: 1px solid #ddd;
}
幾個(gè)月之后可能你已經(jīng)忘記了這個(gè)列表的代碼了,然后你寫(xiě)了 className="row"
在另外的組件上,那么這個(gè)新的組件有了內(nèi)邊距合邊框樣式,你甚至都不知道為什么會(huì)這樣。你可以使用更長(zhǎng)的類(lèi)名或者更加明確的選擇器來(lái)避免這樣的情況發(fā)生,但是你還是無(wú)法完全保證不會(huì)再出現(xiàn)這樣的樣式?jīng)_突。
CSS-in-JS 就可以通過(guò) Locally-scoped styles 來(lái)完全解決這個(gè)問(wèn)題。如果你的列表代碼這么寫(xiě)的話(huà):
<div className={css`
padding: 0.5rem;
border: 1px solid #ddd;
`}>
...row item...
</div>
這樣的話(huà),內(nèi)邊距和邊框的樣式永遠(yuǎn)不會(huì)影響到其他組件。
提示:CSS Modules 也提供了 Locally-scoped styles
2. Colocation: 你的 React 組件是寫(xiě)在 src/components
目錄中的,當(dāng)你裸寫(xiě) CSS 的時(shí)候,你的 .css 文件可能是放置在 src/styles
目錄中。隨著項(xiàng)目越來(lái)越大,你很難明確哪些 CSS 樣式是用在哪些組件上,這樣最后你會(huì)冗余很多樣式代碼。
一個(gè)更好的組織代碼的方式可能是將相關(guān)的代碼文件放在同個(gè)地方。這種做法成為「共置」,可以通過(guò)這篇文章了解一下。
問(wèn)題在于其實(shí)很難實(shí)現(xiàn)所謂的「共置」。如果在項(xiàng)目中裸寫(xiě) CSS 的話(huà),你的樣式和可能會(huì)作用于全局不管你的 .css 文件被放置在哪里。另一方面,如果你使用 CSS-in-JS,你可以直接在 React 組件內(nèi)部書(shū)寫(xiě)樣式,如果組織得好,那么你的項(xiàng)目的可維護(hù)性將大大提升。
提示:CSS Modules 也提供了「共置」的能力
3. 在樣式中使用 JavaScript 變量: CSS-in-JS 提供了讓你在樣式中訪(fǎng)問(wèn) JavaScript 變量的能力
function App(props) {
const color = "red";
const ErrorMessageRed = styled.div`
color: ${props.color || color};
font-weight: bold;
`;
return (
<div>
<ErrorMessageRed>
hello ErrorMessageRed !!
</ErrorMessageRed>
</div>
);
}
上面的例子展示了,我們可以在 CSS-in-JS 方案中使用 JavaScript 的 const 變量 或者是 React 組件的 props。這樣可以減少很多重復(fù)代碼,當(dāng)我們需要同時(shí)在 JavaScript 和 CSS 兩側(cè)定義相同的變量的時(shí)候。我們通過(guò)這樣的能力可以不需要使用 inline styles 這樣的方式來(lái)完成高度自定義的樣式。( inline styles 對(duì)性能不是特別友好,當(dāng)我們有很多相同的樣式寫(xiě)在不同的組件的時(shí)候)
中立點(diǎn)
1. 這是熱門(mén)的新技術(shù): 許多的開(kāi)發(fā)者包括我自己,會(huì)更熱衷于使用 JavaScript 社區(qū)中熱門(mén)的新技術(shù)。一個(gè)重要的原因是,很多新的框架或者庫(kù),能夠提升帶來(lái)巨大的性能或者體驗(yàn)上的提升(想象一下,React 對(duì)比 jQuery 帶來(lái)的開(kāi)發(fā)效率提升)。另一個(gè)原因就是,我們對(duì)新技術(shù)抱有比較開(kāi)放的態(tài)度,我們不愿意錯(cuò)過(guò)每個(gè)大事件。當(dāng)然了,我們?cè)谶x擇新的技術(shù)的時(shí)候也會(huì)考慮到它帶來(lái)的負(fù)面影響。這大概就是我之前選擇 CSS-in-JS 的原因。
缺點(diǎn)
- CSS-in-JS 的運(yùn)行時(shí)問(wèn)題。當(dāng)你的組件進(jìn)行渲染的時(shí)候,CSS-in-JS 庫(kù)會(huì)在運(yùn)行時(shí)將你的樣式代碼 ”序列化” 為可以插入文檔的 CSS 。這無(wú)疑會(huì)消耗瀏覽器更多的 CPU 性能
-
CSS-in-JS 讓你的包體積更大了。 這是一個(gè)明顯的問(wèn)題。每個(gè)訪(fǎng)問(wèn)你的站點(diǎn)的用戶(hù)都不得不加載關(guān)于 CSS-in-JS 的 JavaScript。Emotion 的包體積壓縮之后是 7.9k ,而 styled-components 則是 12.7 kB 。雖然這些包都不算是特別[圖片上傳中...(image-607491-1689056587535)]
大,但是如果再加上 react & react-dom 的話(huà),那也是不小的開(kāi)銷(xiāo)。 -
CSS-in-JS 讓 React DevTools 變得難看。 每一個(gè)使用
css
prop 的 react 元素, Emotion 都會(huì)渲染成<EmotionCssPropInternal>
和<Insertion>
組件。如果你使用很多的css
prop,那么你會(huì)在 React DevTools 看到下面這樣的場(chǎng)景
- 頻繁的插入 CSS 樣式規(guī)則會(huì)迫使瀏覽器做更多的工作。 React 團(tuán)隊(duì)核心成員&React Hooks 設(shè)計(jì)者 Sebasian 寫(xiě)了一篇關(guān)于 CSS-in-JS 庫(kù)如何與 React 18 一起工作的文章。他特別說(shuō)到
在 concurrent 渲染模式下,React 可以在渲染之間讓出瀏覽器的控制權(quán)。如果你為一個(gè)組件插入一個(gè)新的 CSS 規(guī)則,然后 React 讓出控制權(quán),瀏覽器會(huì)檢查這個(gè)新的規(guī)則是否作用到了已有的樹(shù)上。所以瀏覽器重新計(jì)算了樣式規(guī)則。然后 React 渲染下一個(gè)組件,該組件發(fā)現(xiàn)一個(gè)新的規(guī)則,那么又會(huì)重新觸發(fā)樣式規(guī)則的計(jì)算。
實(shí)際上 React 進(jìn)行渲染的每一幀,所有 DOM 元素上的 CSS 規(guī)則都會(huì)重新計(jì)算。這會(huì)非常非常的慢
更壞的是,這個(gè)問(wèn)題好像是無(wú)解的(針對(duì)運(yùn)行時(shí) CSS-in-JS)。運(yùn)行時(shí) CSS-in-JS 庫(kù)會(huì)在組件渲染的時(shí)候插入新的樣式規(guī)則,這對(duì)性能來(lái)說(shuō)是一個(gè)很大的損耗。
- 使用 CSS-in-JS ,會(huì)有更大的概率導(dǎo)致項(xiàng)目報(bào)錯(cuò),特別是在 SSR 或者組件庫(kù)這樣的項(xiàng)目中。在 Emotion 的 GitHub 倉(cāng)庫(kù),我們可以看到很多向如下的 issue
我在我的 SSR 項(xiàng)目中使用了 Emotion,但是它報(bào)錯(cuò)了,因?yàn)椤?
在這些海量的 issue 中,我們可以找到一些共同特征:
- 多個(gè) Emotion 實(shí)例被同時(shí)加載。如果多個(gè)被同時(shí)加載的實(shí)例是相同的Emotion 版本,這將會(huì)引起很多問(wèn)題(比如說(shuō))
- 組件庫(kù)通常無(wú)法讓您完全控制插入樣式的順序(比如說(shuō))
- Emotion 的 SSR 能力支持對(duì)于 React 17 和 18 兩個(gè)版本是不相同的。我們需要做一些兼容性的工作來(lái)兼容 React 18 的 stream SSR(比如說(shuō))
相信我,上述的這些問(wèn)題僅僅是冰山一角。
性能檢測(cè)
在這一點(diǎn)上,很明顯,CSS-in-JS 有著顯著的優(yōu)點(diǎn)和缺點(diǎn)。為了明白我們?yōu)槭裁凑谝瞥@項(xiàng)技術(shù),我們需要更加真實(shí)的 CSS-in-JS 性能場(chǎng)景。這里我們會(huì)著重關(guān)注 Emotion 對(duì)于性能的影響。Emotion 有很多種使用方式,每種方式都有其各自的性能表現(xiàn)特點(diǎn)。
內(nèi)部序列化渲染 vs. 外部序列化渲染
樣式序列化指的是 Emotion 將你的 CSS 字符串或者樣式對(duì)象轉(zhuǎn)化成可以插入文檔的純 CSS 字符串。Emotion 同時(shí)也會(huì)在序列化的過(guò)程中根據(jù)生成的存 CSS 字符串計(jì)算出相應(yīng)的哈希值——這個(gè)哈希值就是你可以看到的動(dòng)態(tài)生成的類(lèi)名,比如 css-an61r6
在測(cè)試前,我預(yù)感到這個(gè)樣式序列化是在 React 組件渲染周期里面完成還是外面完成,將對(duì) Emotion 的性能表現(xiàn)起到比較大的影響。
在渲染周期內(nèi)完成的代碼如下
function MyComponent() {
return (
<div
css={{
backgroundColor: 'blue',
width: 100,
height: 100,
}}
/>
);
}
每次 MyComponent
渲染,樣式對(duì)象都會(huì)被序列化一次。如果 MyComponent
渲染的比較頻繁,重復(fù)的序列化將有很大的性能開(kāi)銷(xiāo)
一個(gè)性能更好的方案是把樣式移到組件的外面,所以序列化過(guò)程只會(huì)在組件模塊被載入的時(shí)候發(fā)生,而不是每次都要執(zhí)行一遍。你可以使用 @emotion/react
的 css
方法
const myCss = css({
backgroundColor: 'blue',
width: 100,
height: 100,
});
function MyComponent() {
return <div css={myCss} />;
}
當(dāng)然,這樣使得你無(wú)法在樣式種獲得組件的 props,所以你會(huì)錯(cuò)失 CSS-in-JS 的一個(gè)主要的賣(mài)點(diǎn)。
測(cè)試「成員檢索」功能
我們接下來(lái)將使用在一個(gè)頁(yè)面上實(shí)現(xiàn)「成員檢索」的能力,就是使用一個(gè)列表展示團(tuán)隊(duì)成員的一個(gè)簡(jiǎn)單的功能。列表上幾乎所有的樣式都是通過(guò) Emotion 來(lái)實(shí)現(xiàn),特別是使用 css
prop
測(cè)試如下:
- 「成員檢索」會(huì)在頁(yè)面上顯示 20 個(gè)用戶(hù)
- 去除
react.memo
對(duì)列表的包裹 - 每秒都強(qiáng)制渲染 <BrowseMembers> 組件,記錄前 10 次渲染的時(shí)間
- 關(guān)閉 React Strict 模式 (不然會(huì)觸發(fā)重復(fù)渲染,時(shí)間可能是現(xiàn)在的 2 倍)
我使用 React DevTools 進(jìn)行記錄,得到前 10 次的平均渲染時(shí)間為 54.3 毫秒。
以往的經(jīng)驗(yàn)告訴我,一個(gè) React 組件最好的渲染時(shí)間大概是 16 毫秒(每秒 60 幀計(jì)算)。 < BrowseMembers >
組件的渲染時(shí)間是經(jīng)驗(yàn)值的 3 倍左右,所以它是一個(gè)比較「重」的組件。
如果我去除 Emotion,而使用 Sass Modules 來(lái)實(shí)現(xiàn)頁(yè)面的樣式,平均的渲染時(shí)間大概是在 27.7 毫秒。這比原來(lái)使用 Emotion 少了將近 48% !!!
這就是為什么我們開(kāi)始放棄使用 CSS-in-JS 的原因:運(yùn)行時(shí)的性能消耗實(shí)在太嚴(yán)重了!!!
我們的新樣式方案
在我們下定決心要移除 CSS-in-JS 之后,剩下的問(wèn)題就是:我們應(yīng)該什么方案來(lái)代替。我們既想要有裸寫(xiě) CSS 這樣的性能,又想要盡可能保留 CSS-in-JS 的優(yōu)點(diǎn)。這里再次簡(jiǎn)單梳理一下 CSS-in-JS 的優(yōu)點(diǎn)(忘記的同學(xué)可以翻回上面再看看):
- locally-scoped styles
- colocated
- 在 CSS 中使用 JS 變量
如果你有認(rèn)真看這篇文章,那你應(yīng)該還記得我在上文中提到,CSS Modules 其實(shí)也是可以提供 locally-scoped styles 和 colocated 這樣類(lèi)似的能力的。并且 CSS Modules 編譯成原生 CSS 文件之后,沒(méi)有運(yùn)行時(shí)的性能開(kāi)銷(xiāo)。
在我看來(lái),CSS Modules 的缺點(diǎn)在于,他們依然是原生的 CSS —— 原生 CSS 缺少提升開(kāi)發(fā)體驗(yàn)以及減少冗余代碼的能力。但是,如果當(dāng)原生CSS 具備 nested selectors 的能力之后,情況將會(huì)改善很多。
幸好,市面上已經(jīng)有了一個(gè)很簡(jiǎn)單的方案來(lái)解決這個(gè)問(wèn)題—— Sass Modules ( 使用 Sass 來(lái)寫(xiě) CSS Modules ) 。你既可以享受 CSS Modules 的 locally-scoped styles 能力,又可以享受 Sass 強(qiáng)大的編譯時(shí)功能(去除運(yùn)行時(shí)性能開(kāi)銷(xiāo))。這就是我們會(huì)使用 Sass Modules 的一個(gè)重要原因。
注意:使用 Sass Modules ,你將無(wú)法享受到 CSS-in-JS 的第 3 個(gè)優(yōu)點(diǎn)(在 CSS 中使用 JS 變量)。但是你可以使用
:export
塊將 Sass 代碼的常量導(dǎo)出到 JS 代碼中。這個(gè)用起來(lái)不是特別方便,但是會(huì)使你的代碼更加清晰。
Utility Classes
比較擔(dān)心我們團(tuán)隊(duì)從 Emotion 切換到 Sass Modules 之后,會(huì)在寫(xiě)一些極度常用的樣式的時(shí)候不是很方便,比如 display: flex
。之前我們是這樣寫(xiě)的
<FlexH alignItems="center">...</FlexH>
如果改用 Sass Modules 之后,我們需要?jiǎng)?chuàng)建一個(gè) .module.scss
文件,然后寫(xiě)一個(gè) display: flex
和 align-item: center
。這不是世界末日,但肯定是不夠方便的。
為了提升開(kāi)發(fā)體驗(yàn),我們決定引入一個(gè) Utility Classes。如果你對(duì) Utility Classes 還不是很熟悉,用一句話(huà)概括就是,“他們是一些只包含一個(gè) CSS 屬性的 CSS 類(lèi)”。通常情況下,你會(huì)在你的元素上使用多個(gè)這樣的類(lèi),通過(guò)組合的方式來(lái)修改元素的樣式。對(duì)于上面的這個(gè)例子,你可能需要這樣寫(xiě):
<div className="d-flex align-items-center">...</div>
Bootstrap 和 Tailwind 是目前最流行的提供 Utility Classes 的解決方案。這些庫(kù)在設(shè)計(jì)方案上做了非常多的努力,這使得我們可以放心的使用他們,而不是自己重新搭建一個(gè)。因?yàn)槲沂褂?Bootstrap 已經(jīng)很多年了,所以我們選擇了 Bootstrap。我們使用 Bootstrap 作為我們項(xiàng)目的預(yù)設(shè)樣式方案。
我們已經(jīng)在新組件上使用 Sass Modules 和 Utility Classes 好幾個(gè)星期了。我們覺(jué)得都不錯(cuò)。它的開(kāi)發(fā)體驗(yàn)跟 Emotion 差不多,但是運(yùn)行時(shí)的性能更加的好。
我們也使用 typed-scss-modules 來(lái)為 Sass Modules 生成 TypeScript 的類(lèi)型文件。也許這樣做最大的好處就是允許我們定一個(gè)幫助函數(shù)
utils()
,這樣我們可以像使用 classnames 去操作樣式。
一些關(guān)于 構(gòu)建時(shí) CSS-in-JS 方案
本文主要關(guān)注的是 運(yùn)行時(shí) CSS-in- JS 方案,比如 Emotion 和 styled-components 。最近,我們也關(guān)注到了一些將樣式轉(zhuǎn)換是純 CSS 的構(gòu)建時(shí)CSS-in-JS 方案。包括
這些庫(kù)的目標(biāo)是為了提供類(lèi)似于運(yùn)行時(shí) CSS-in-JS 的能力,但是沒(méi)有性能損耗。
目前我還沒(méi)有在真實(shí)項(xiàng)目中使用構(gòu)建時(shí) CSS-in-JS 方案。但我想這些方案對(duì)比 Sass Modules 大概會(huì)有以下的缺點(diǎn):
- 依然會(huì)在組件 mount 的時(shí)候完成樣式的第一次插入,這還是會(huì)使得瀏覽器重新計(jì)算每個(gè) DOM 節(jié)點(diǎn)的樣式
- 動(dòng)態(tài)樣式無(wú)法被抽取出來(lái),所以會(huì)使用 CSS 變量加上行內(nèi)樣式的方法來(lái)替代。過(guò)多的行內(nèi)樣式依然會(huì)影響性能
- 這些庫(kù)依然會(huì)插入一些特定的組件到項(xiàng)目的 React 樹(shù)中,依然會(huì)導(dǎo)致 React DevTools 的可讀性變得比較差
結(jié)論
感謝你閱讀到這里~任何事情都是,有它好的一面也有它不好的一面。最終,作為開(kāi)發(fā)人員,你必須評(píng)估這些優(yōu)缺點(diǎn),然后就該技術(shù)是否適合你的項(xiàng)目,然后做出決定。而對(duì)于目前我所在的團(tuán)隊(duì)來(lái)說(shuō),Emotion 帶來(lái)的運(yùn)行時(shí)性能消耗的影響已經(jīng)大于它帶來(lái)的開(kāi)發(fā)體驗(yàn)的好處。而我們目前所使用的 Sass Modules 加上 Utility Classes 方案,在一定程度上也彌補(bǔ)了開(kāi)發(fā)體驗(yàn)的問(wèn)題。以上~
本文為翻譯文:
原文地址:https://dev.to/srmagura/why-were-breaking-up-wiht-css-in-js-4g9b