logo

Loading...

avatar
Suica
githubtwitter
關於我
博客
項目
標籤

由倫敦鵝開發

© 2021 倫敦鵝. 保留所有權利。

累積訪問: 12714 次

請不要使用行內樣式 (inline style)

2024-05-23

幾週前,一篇關於行內樣式比導入樣式表更快的文章被發布。許多前端開發者對此感到驚訝,並建議也許我們可以像 Tailwind CSS 一樣將樣式方法編譯成行內樣式。我也很驚訝,框架如此流行以至於人們忘記了網站樣式的歷史以及瀏覽器如何渲染網站。

瀏覽器如何渲染網站

讓我們從我在中國的第一份初級前端工作的面試問題開始:在瀏覽器中輸入 URL 後會發生什麼?

  1. 瀏覽器會檢查用戶是在搜索內容還是在輸入 URL。
  2. 域名解析,找到網站服務器的 IP 地址。
  3. 與服務器建立 TCP 連接。(TSL 加密和三次握手)
  4. 瀏覽器發起 HTTP 請求。
  5. 服務器解析並響應請求,發回 HTML 內容。
  6. 瀏覽器解析 HTML 代碼並從 HTML 代碼中請求資源(CSS、JavaScript、圖片等)。
  7. 用戶可以看到網站的內容。

你可能知道為什麼行內樣式和行內 CSS 比外部樣式表更快。導入外部樣式表會為 CSS 文件創建一個新的 HTTP 請求。由於發起新的 HTTP 請求的成本,兩個順序運行的 HTTP 請求絕對比相同大小的單個請求慢。此外,最大的影響是,直到瀏覽器解析到導入樣式表的行時,才會發送樣式表的 HTTP 請求。

我們可以從瀏覽器如何渲染網站來檢查這一點。

  1. 解析 HTML 並生成 DOM 樹
    1. 構建 DOM 樹
    2. 加載資源。當遇到相關標籤時,瀏覽器會將請求委託給網絡線程。
    3. 如果遇到 <script></script> 標籤且沒有 defer 屬性,則停止解析並運行 JS。
  2. 計算樣式並構建 CSSOM 樹
    1. 瀏覽器將解析 CSS 並根據選擇器的優先級計算樣式。
    2. 即使你沒有任何 CSS,它也會為你生成瀏覽器默認的計算樣式。
  3. 構建佈局樹。
  4. 遍歷佈局樹並生成繪製記錄。
  5. 柵格化

結果很明顯:外部樣式表的請求總是比解析 HTML 慢。因此,外部樣式表絕對比行內樣式/CSS 慢。此外,行內 CSS 還涉及將類名鏈接到樣式的額外步驟,這會產生性能成本。因此,性能結果是直觀的:

行內樣式 > 行內 CSS > 外部樣式表

Load CSS

然而,為什麼我們不使用行內樣式?

CSS 樣式的歷史

眾所周知,在網頁開發的早期階段,沒有前端開發者的角色。前端代碼是後端代碼的一部分,只是 MVC 模型中的 View。隨著 Ajax 的誕生,一切都改變了,前端開發者變得越來越重要。

作為一個獨立的項目,前端開發者需要考慮更多因素。首要考慮的是用戶體驗。此外,他們還需要關注提高可維護性、最大化瀏覽器緩存使用以及管理網站版本。

因此,行內樣式方法立即被禁止。這些方法有幾個問題:

1. 難以維護

編寫行內樣式不容易維護。在前端開發的早期階段,編輯器缺乏適當的插件來提高行內樣式的可讀性。你不能添加縮進來格式化代碼,因為壓縮工具無法移除它。

<h1 style="color:blue;text-align:center;">這是一個標題</h1>
<p style="color:red;">這是一個段落。</p>

此外,隨著樣式的增長,可讀性和可維護性會惡化。例如,如果你想寫一個博客,主要使用 <p></p> 標籤,你需要對每個標籤應用相同的樣式,這對包大小和可維護性都不利。

這個問題在今天的 Tailwind CSS 中很常見,但我們可以通過創建組件和使用 @apply 來分組樣式,提高可維護性並減少包大小。

誠然,如果你只想更新一個標籤的樣式,很容易找到並更改行內樣式。然而,這種方法仍然對包大小產生負面影響。

2. 可重用性和包大小

如前所述,沒有工具可以壓縮縮進,行內樣式對可重用性非常不利。隨著項目的增長,這個問題變得越來越嚴重。

3. 不利於更新樣式和利用緩存

除了增加包大小外,它還影響版本控制和緩存。如果你把所有內容都寫成行內樣式,每當你想更改樣式或佈局時,你必須更新整個 HTML 和 CSS 文件。這意味著客戶端必須從服務器請求所有新內容,而不是部分更新 HTML 和 CSS。

這也限制了你自己的設計系統的實現。如果你不能重用樣式,你怎麼能重用設計?

現代化

在智能手機興起之前,jQuery 和 Bootstrap 主導了前端。與此同時,Node.js 引入了前端模塊化的可能性。隨著單頁應用程序(SPA)的流行,出現了不同的 CSS 解決方案,包括模塊 CSS、CSS 預處理器(如 SCSS 和 LESS)和 CSS-in-JS。這些解決方案都解決了某些問題,但也有其局限性。沒有一個成為明確的贏家。

CSS 預處理器

優點:

  1. 提供 CSS 沒有的更多功能。
  2. 為 CSS 添加編程功能
  3. 可以編譯成 CSS,兼容性問題較少

缺點:

  1. 學習曲線高
  2. 對團隊要求高
  3. 需要編譯,減慢構建速度
  4. 調試困難,因為編譯後的文件很複雜。
  5. 使用 mixin 等功能時會增加複雜性

模塊 CSS

優點:

  1. 易於維護
  2. 樣式可重用
  3. 解耦
  4. 避免全局樣式污染。
  5. 可以與 CSS 預處理器一起使用

缺點:

  1. 難以與 UI 庫協作
  2. 需要管理導入

CSS in JS

優點:

  1. 完全沒有樣式衝突
  2. 與 JS 結合,固定到模塊中
  3. 易於將 JS 變量傳入 CSS
  4. 類型安全

缺點:

  1. 運行時性能問題,特別是在 React 18 之後
  2. 包大小
  3. 破壞 React DevTools,因為它們會用另一層包裝組件來將樣式注入到你的組件中。

原子化

如果你搜索當下最流行的 CSS 解決方案,Tailwind CSS 會是首選。雖然它起源於類似 Bootstrap 的概念,但它更接近於另一種稱為 Atomic CSS 的方法。

優點

它解決了什麼問題?

  1. CSS 的終極可重用性
  2. 最小的包大小(可以更好地利用 gzip)
  3. 沒有樣式污染
  4. 在大多數情況下不需要命名(不要使用 @apply)
  5. 編譯速度快
  6. 開發速度快
  7. 易於複製組件
  8. 易於定義主題
  9. 易於學習
  10. 易於構建自己的設計系統,因此有強大的生態系統

Tailwind 實際上是前端開發的一個改變者。它解決了其他 CSS 解決方案中的許多問題。它是可擴展的且易於使用,但它完美嗎?

絕對不是。

Tailwind CSS 有幾個問題:

  1. 很難將 JS 變量傳入樣式,因為它會破壞 Tailwind CSS 的樹搖規則。
  2. 當 UI 變得複雜時很難閱讀
  3. 不能對樣式進行註釋
  4. 更改配置很煩人,有時需要重啟整個開發服務器
  5. 不能使用 calc()

缺點

我是那種總是同意犧牲開發者體驗(DX)來改善用戶體驗(UX)的開發者。可讀性對我來說不是一個重要問題。我的解決方案之一是通過功能將長類名字符串分成較小的組。這種方法確實有一些性能成本,但它們是最小的。

<div className="bg-slate-100 rounded-xl p-8 dark:bg-slate-800"/>

<div className={twMerge(
   "bg-slate-100 dark:bg-slate-800",
   "rounded-xl p-8"
   )}
/>

Tailwind CSS 的最大問題是它不支持動態樣式,因為所有樣式都是在構建時生成的。如果我們需要編寫與 JavaScript 交互的樣式,例如複雜的樣式或動畫,我們必須使用行內樣式或像 framer-motion 這樣的庫,它也依賴於行內樣式。

回到行內樣式

本文故意不討論 CSS 優先級,因為這是前端開發者長期以來面臨的常見問題(而且我不想離題)。

CSS 的優先級可以簡化如下:

行內 > 內部 > 外部

Id > Class > Tag

最後定義 > 首先定義

許多開發者避免編寫 CSS,因為他們覺得它太難太複雜,這造成了很大的心理負擔。在傳統方法如 CSS Modules 或 SCSS 中,開發者可能需要覆蓋第三方庫或同事的樣式。在這種情況下,如果有人使用行內樣式,這可能會非常令人沮喪。Tailwind 通過允許你使用像 Tailwind Merge 這樣的庫來合併組件內的類名,部分解決了這個問題。

<div
  className={twMerge(
    "bg-slate-100 dark:bg-slate-800",
    props.className // 來自 props 的類名
  )}
/>

為什麼我現在提到行內樣式的優先級?

行內樣式一直因為具有最高優先級而受到批評,使其難以覆蓋。雖然我們可以使用 !important 來給樣式最高優先級並覆蓋行內樣式,但如果我需要覆蓋一個已經使用 !important 的樣式怎麼辦?我該如何管理樣式,使它們都具有相同的優先級?

這揭示了 Tailwind 的另一個問題。它並沒有從根本上解決優先級管理問題。相反,它嚴重依賴生態系統和你的編碼習慣來管理優先級。仍然需要代碼標準和團隊代碼審查。

下一步是什麼?

關注點分離是 Atomic CSS 的核心概念之一;HTML 和 CSS 應該遵循單一職責原則。Tailwind 解決了 Atomic CSS 的一些問題並改善了開發者體驗(DX)。

下一步是什麼?

前端開發者最喜歡的解決方案之一:將問題委託給編譯器。

Facebook 去年年底推出了一個名為 StyleX 的庫。它使用 CSS-in-JS 的語法,並包含一個編譯器,將樣式轉換為 Atomic CSS。

它實現了確定性解析,確保最後的調用始終是正確的。此外,它提供了 Tailwind 缺乏的功能:類型安全。

const styles = stylex.create({
  base: {
    fontSize: 14,
    color: "rgb(0,0,0)",
  },
  highlighted: {
    color: "red",
  },
});

讓我們回到 CSS-in-JS 的缺點:

  1. 運行時性能問題,特別是在 React 18 之後
  2. 包大小
  3. 妥協的 React DevTools,因為它們會用另一層包裝組件來將樣式注入到你的組件中。

所有這些問題都被 Atomic CSS 解決了,StyleX 也具有 CSS-in-JS 的優點。我不能抱怨更多了。聽起來很完美,不是嗎?

不完全是。與 Tailwind 相比,它仍然有一些問題。一個是它的語法更難學習。另一個缺陷,Tailwind 也有,是重繪和重排性能。例如,我們有一個控制組件不同樣式的狀態。

function Header() {
   const [isLoading, setIsLoading] = useState(false)

   useEffect(() => {
      setIsLoading(true)
   }, [])

   return (
      <header>
         <h1 className={isLoading ? 'loading-logo' : 'loaded-logo'}>
          Scss Logo
         </h1>
         <h1 className={clx({
            'text-red font-bold translate-x-1':
            'text-blue font-thin -translate-x-1':
         })}>
            Tailwind Logo
         </h1>
      </header>
   )
}

在評估樣式更改的性能時,Tailwind 明顯比 SCSS 慢。原因很明顯:像 Tailwind 這樣的 Atomic CSS 解決方案需要同時更改多個類,而傳統的 CSS 解決方案在單個操作中更新樣式。這種性能差異通常可以忽略不計,除非遇到特定的性能瓶頸。在 Tailwind 中,可以使用 @apply 指令來分組樣式並緩解這個問題。然而,StyleX 目前缺乏解決這個問題的方案。

結論

本文探討了 CSS 樣式的歷史,以確定當前可用的最佳方法。雖然 StyleX 似乎是當今最好的解決方案,但它可能不會永遠如此。隨著 StyleX 的廣泛使用,可能會出現其他問題。未來是不確定的,沒有人能預測會發生什麼。隨著技術的不斷進步,開發者可能很快就會發現利用行內樣式來解決現有挑戰的方法。

2014 年的前端開發者:不要使用 cookie,使用 localStorage 和 header 來保存 token

2024 年的前端開發者:不要使用 localStorage 和 header 來保存 token,使用 cookie

但至少在今天,請不要這樣做。

HistoryTailwind CSSStyleXPerformanceBrowser RenderingCSS
請不要使用行內樣式 (inline style) | Suica's blog