JS生態(tài)系統(tǒng)加速Tailwind?CSS工作原理探究
引言
長話短說:自破蛋以來,Tailwind CSS 已成為一種人氣爆棚的 Web 項(xiàng)目樣式方案。這次我們來瞄一下為其提供支持的架構(gòu),以及可以優(yōu)化的方案。
本期《前端翻譯計(jì)劃》共享的是“加速 JS 生態(tài)系統(tǒng)系列博客”,包括但不限于:
PostCSS,SVGO 等等
模塊解析
使用 eslint
npm 腳本
draft-js emoji 插件
polyfill 暴走
桶裝文件暴走
Tailwind CSS
Tailwind CSS
本期共享的是第 8 篇博客 —— Tailwind CSS 方案。
誠然,我目前手頭沒有訴諸 Tailwind CSS 編寫的大型項(xiàng)目。我那些使用 Tailwind 的項(xiàng)目太小,由此得出的性能分析不具備統(tǒng)計(jì)學(xué)意義。所以我有一個大膽的想法:用 Tailwind 自己的 tailwindcss.com 官網(wǎng)介紹 Tailwind 簡直絕絕子!不過在下出師未捷身先死:Tailwind 官網(wǎng)訴諸 Next.js 構(gòu)建,要獲得有意義的調(diào)試比脫單還難。更重要的是,這些調(diào)試摻雜了一大坨與 TailwindCSS 毫無關(guān)系的干擾。
退而求其次,我決定使用完全相同的配置在項(xiàng)目上運(yùn)行 Tailwind CLI,從而獲取某些性能追蹤。運(yùn)行 CLI 構(gòu)建總共需要 3.2 秒,而 Tailwind 在運(yùn)行時花費(fèi)了 1.4 秒。如下所示,我們可以找出某些時間開銷的性能重災(zāi)區(qū):
這里火焰圖的 x 軸不表示“發(fā)生時”的時間,而表示此處合并在一起的每個調(diào)用堆棧的累積時間。性能重災(zāi)區(qū)一目了然。我正在使用 SpeedScope 來可視化 CPU 配置文件。
有一個處理提取潛在的解析候選的區(qū)塊,一個配置和插件初始化的區(qū)塊,CSS 生成,某些 PostCSS 的東東,當(dāng)有 PostCSS 時,通常同時提及 autoprefixer,因?yàn)閮烧呓?jīng)常夢幻聯(lián)動。粉絲請注意,在不執(zhí)行任何操作的情況下加載 autoprefixer 似乎已經(jīng)消耗了一大坨時間。
轉(zhuǎn)換思路
瞄一下 Tailwind CSS 代碼庫,查看配置文件,肯定存在某些函數(shù)可以繼續(xù)優(yōu)化的地方。但如果我們這樣做,我們能且僅能斬獲幾個個位數(shù)的百分比優(yōu)化。
實(shí)現(xiàn)多因素加速、而不僅僅是低百分比提速的秘訣,不在于應(yīng)用通用規(guī)則或習(xí)慣,比如“不要在 for
循環(huán)里創(chuàng)建閉包”。這是一個常見的誤解,我們認(rèn)為如果遵循所有這些“最佳實(shí)踐”,代碼就會變快,因?yàn)樵诖蠖鄶?shù)情況下(并非全部),令人不安的事實(shí)是,這絕非關(guān)鍵優(yōu)化。使代碼變快的原因是,充分理解代碼的作用,然后采取最短路徑實(shí)現(xiàn)該目標(biāo)。
因此,作為一個挑戰(zhàn),私以為如果我們兼顧性能從零構(gòu)建,那么看看 Tailwind 代碼的架構(gòu)會很有趣。我們會做出不同的決定嗎?但為了找到最佳架構(gòu),我們需要知道 Tailwind 解決的是哪個問題,并考慮實(shí)現(xiàn)該目標(biāo)的最短路徑。
Tailwind CSS 工作原理
從本質(zhì)上講,Tailwind CSS 的工作機(jī)制是,我們向它傳遞某些 CSS 文件,然后它在其中查找 @tailwind
規(guī)則。如果它邂逅匹配的規(guī)則,那么它會爬取項(xiàng)目中的其他文件,查找 tailwind 類名,并將其注入到找到該 @tailwind
規(guī)則的 CSS 文件中。它還有其他方方面面,但為了簡單起見,我們暫且無視其他規(guī)則。
/* 輸入 */ @tailwind base; @tailwind components; @tailwind utilities; .foo { color: red; }
這會被轉(zhuǎn)化為:
.border { border-width: 1px; } .border-2 { border-width: 2px; } /* 等等...... */ .foo { color: red; }
基于此機(jī)制,我們可以確定 Tailwind CSS 內(nèi)部流程的若干階段:
掃描
.css
文件中的@tailwind
規(guī)則基于用戶 tailwind 配置中提供的 glob 模式,查找所有文件,從中提取 tailwind 類名
一旦找到這些文件,就會提取潛在的 tailwind 類名
解析潛在的 tailwind 類名,檢查它們是否是 tailwind 類名。如果是,那就從中生成某些 CSS
將原本 css 文件中的
@tailwind
規(guī)則替換為生成的 CSS
優(yōu)化提取階段
由于有且僅有三個有效的 @tailwind
規(guī)則值,我們可以使用一個基本的正則,繞過整個 PostCSS 解析步驟:
;/@tailwind\s+(base|components|utilities)(?:;|$)/gm
雖然但是,一旦讀取了那些文件,且我們需要提取潛在的 tailwind 類名候選,我們就有優(yōu)化空間。但有一個問題:我們?nèi)绾闻袛嗪蜻x是否為 tailwind 類名?這表面上易如反掌,但實(shí)際上比脫單還難。問題在于,沒有作者或任何其他證據(jù)表明,字符序列乃有效的 tailwind 類名。可能存在與 tailwind 類名具有相同格式、但不存在的單詞組合。
舉個栗子,有效的 tailwind 類名如下所示:
ml-2
border-b-green-500
dark:text-slate-100
dark:text-slate-100/50
[&:not(:focus-visible)]:focus:outline-none
那么 foo-bar
是有效的 tailwind 類名嗎?它并非 tailwind 默認(rèn)語法的一部分,但它可以由用戶添加。因此,我們在這里有且僅有的真正選擇是,盡量減少搜索空間,然后向解析器“投喂”剩余的候選。如果解析器生成了某些 CSS,那么我們就知道類名有效。反之無效。這反過來意味著,我們需要優(yōu)化解析器,在檢測到?jīng)]有定義的字符串值時,盡快退出。
粉絲請注意:目前在 Tailwind CSS 中,這大約需要 388ms
。
我在本地給 Tailwind CSS 打補(bǔ)丁,顯示了某些有關(guān)提取器的提取值的統(tǒng)計(jì)數(shù)據(jù)。
已解析文件:
454
候選字符串:
26_466
但更有趣的是,瞄一下提取程序提取最常見的前 10 個值:
- 9774x ''
- 2634x </div>
- 1858x }
- 1692x ```
- 1065x },
- 820x ---
- 694x ```html
- 385x {
- 363x >
- 345x </p>
換而言之,在 26_466
個匹配的字符串中,其中 19_630
個顯然是無效的 tailwind 類名。平心而論,Tailwind CSS 存在某些緩存,可以減輕檢查某些東東是否存在“假陽性”。并且已經(jīng)有一個代碼注釋道,對其正則的任何優(yōu)化,都能將 Tailwind CSS 提速高達(dá) 30%。
萬物皆可正則
這里使用正則的“雙刃劍"是,它不具有語言感知能力。它不知道我們是在 .js
還是 .html
文件上操作,更糟糕的是,該語言還可以互相嵌入。.html
文件可以同時托管 HTML、JS 和 CSS。.jsx
文件中的 JSX 同理可得。當(dāng)涉及 JS 代碼時,我們可以假設(shè)我們只需查看字符串。
經(jīng)過簡單粗暴的正則處理后,我們將搜索空間從 26_466
減少到 9_633
個候選。這仍不是極致優(yōu)化,但比我們開始時要更勝一籌?,F(xiàn)在,一大坨提取字符串類似于更多潛在的 tailwind 候選字符串:
relative not-prose [a:not(:first-child)>&]:mt-12
none
break-after
grid-template-rows
...
每個提取字符串都包含一個或多個潛在候選。我們可以通過在每個提取字符串上觸發(fā)另一個正則,繼續(xù)減少搜索空間,提取可能是有效 tailwind 類名的部分。對我們而言幸運(yùn)的是,有效的 tailwind 類名的語法遵循相當(dāng)簡單的規(guī)則:
- 禁用空格
- 變體必須以
:
冒號結(jié)尾 - 任意值訴諸
[foo]
括號定義。它們必須位于類名末尾 - 變體任意:
[&>.foo]:border-2
。禁止包含空格 - 除括號內(nèi)的值之外的其他東東,只能包含數(shù)字、字母字符或減號。我不確定是否允許下劃線,但我猜它可能是用戶定義的 tailwind 類名
- 有效的 Tailwind 類名必須以
[
、-
、!
、a-z
或0-9
開頭
所有這些匹配客觀存在某些時間開銷,并將總提取時間增加到 92ms
。在努力減少搜索空間后,我們?nèi)允O麓蠹s 8_000
個潛在的 tailwind 類名(粉絲請記住,之前提取的字符串可以包含多個候選)。
目前為止,我們斬獲了值得褒獎的成果。我們將提取時間從 Tailwind 的原本 388ms
減少到 98ms
。這大約優(yōu)化了 4 倍。
類名轉(zhuǎn) CSS
在這個階段,我們尚未生成任何 CSS 規(guī)則。我們?nèi)孕枰承┮?guī)則,替換起初原始 CSS 文件中的 @tailwindcss
規(guī)則。但我們現(xiàn)在可以訴諸潛在的 tailwind 類名列表來實(shí)現(xiàn)。其中一大坨可能是“假陽性”,因此我們需要確保,如果我們檢測到不渲染 CSS 的類名,我們可以盡快退出。
第一步是解析前面的變體(如果有的話)。粉絲請記住,可以通過 :
尾冒號字符來檢測變體。變體的要點(diǎn)之一是,如果變體存在,它們能且僅能影響選擇器,且可能影響周圍的媒體查詢。它們本身不用于生成 CSS 屬性。解析變體是一項(xiàng)平平無奇的體力活。如果我們檢測到假定的變體不存在,我們就可以提前退出。
比變體更有趣的是規(guī)則生成方面。大多數(shù) tailwind 類名沒有變體。由于 Tailwind 映射了一大坨 CSS 屬性,因此我們需要匹配的潛在數(shù)量相當(dāng)驚人。我嘗試了各種方案,比如預(yù)先匹配所有靜態(tài) tailwind 類名,將所有內(nèi)容放入一個對象中,該對象的方法類似虛擬函數(shù)表。但最終,私以為既敏捷又易維護(hù)的方案是,一坨既大又笨的 switch
語句。
function parse(lexer, config, hasNegativePrefix) { const first = lexer.nextSegment() switch (first) { case “aspect”: //... case “block”: if (!lexer.isEnd) return // 退出 return `display: block` case “inline”: if (lexer.isEnd) return `display: inline` const second = lexer.nextSegment(); if ( second !== “block” || second !== “flex” || second !== “table” || second !== “grid” ) { return // 退出 } return `display: inline-${second}` // 剩下的 1000 行類似的代碼 } }
這看起來可能是非常標(biāo)準(zhǔn)的解析器代碼,但存在某些有趣的東東。顯而易見,我們會逐步檢查我們是否仍在有效路徑上。這增加了一大坨額外檢查,但我發(fā)現(xiàn)這些成本能夠被提前退出的收益抵消。在之前的某些迭代中,我在提取部分犯錯了,最終向該 parse
函數(shù)“投喂”了一大坨已知的“假陽性”字符串。但是因?yàn)?nbsp;parse
函數(shù)很快就在無效的類名及時止損,所以我花了一段時間才注意到,它整體而言仍然很快。
粉絲請注意傳遞給 parse()
函數(shù)的 hasNegativePrefix
參數(shù)。一大坨數(shù)字筑基的屬性(比如 padding
)可以通過在類名前加上 -
減號字符來接收負(fù)值。
'pl-2' // -> padding-left: 0.5rem; '-pl-2' // -> padding-left: -0.5rem;
前置減號字符在傳遞給 parse()
函數(shù)之前會被移除,這樣我們可以為正?;蚍闯G闆r重用相同的 case
分支。這里沒有顯示,但解析器還支持任意值、important
聲明、透明度的 color
值等等。
盡管我沒有實(shí)現(xiàn)所有規(guī)則,但所有語法變體都支持。不過,我確實(shí)實(shí)現(xiàn)了相當(dāng)一部分規(guī)則,大約有 126
條。這大約占 tailwind 語法的 80%。盡管這主要是一個原型,但我想更好地了解解析器如何擴(kuò)展。
有了生成的規(guī)則,我們現(xiàn)在終于可以替換原始 CSS 文件中的 @tailwind
規(guī)則了。如果我們希望它能夠感知源碼映射,那么我們可以使用 Magic String
。
萬事俱備后,以下是最終測量結(jié)果:
提取:98ms
解析:21ms
總時間:192ms(包括運(yùn)行時啟動時間)
整個項(xiàng)目由 5 個文件組成(不包括測試),代碼不足 3_000
行。
Rust 又如何呢?
我們這里的迷你項(xiàng)目比 og Tailwind CSS cli 更快的原因是,我們完全避免了用 PostCSS 解析任何內(nèi)容,而是聚焦于盡快生成 CSS 規(guī)則。Tailwind 團(tuán)隊(duì)目前正在用 Rust 重寫 Tailwind CSS,據(jù)我所知,它們已經(jīng)取得了很大進(jìn)展。我沒有任何相關(guān)數(shù)據(jù),因?yàn)樗形窗l(fā)布。就像任何訴諸 Rust 重寫的 JS 工具一樣,亟待解決的是它們的插件的生態(tài)。Tailwind 確實(shí)支持在其配置中定義的自定義變體或完整規(guī)則。一旦發(fā)布,測評兩者將會很有趣。
完結(jié)撒花
對我而言,Tailwind CSS 是 CSS 中的 jQuery。并不是所有人都喜歡它,但它為網(wǎng)絡(luò)行業(yè)注入正能量毋庸置疑。它使全新一代開發(fā)者能夠進(jìn)軍 Web 開發(fā)領(lǐng)域。
當(dāng)我入門 Web 開發(fā)時,jQuery 正血?dú)夥絼?,沒有它我就永遠(yuǎn)不會和 JS 貼貼。直到職業(yè)生涯兩年后,我才真正入坑 JS,并學(xué)習(xí)了基礎(chǔ)知識。在 CSS 方面,Tailwind CSS 正在為當(dāng)今的開發(fā)者做類似的事情。
免責(zé)聲明
本文屬于是語冰的直男翻譯了屬于是,略有刪改,僅供粉絲參考,英文原味版請傳送 Speeding up the JavaScript ecosystem - Tailwind CSS[1]。
以上就是JS 生態(tài)系統(tǒng)加速Tailwind CSS工作原理探究的詳細(xì)內(nèi)容,更多關(guān)于JS Tailwind CSS的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
bootstrapValidator.min.js表單驗(yàn)證插件
這篇文章主要為大家詳細(xì)介紹了bootstrapValidator.min.js表單驗(yàn)證插件的使用方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-02-02JavaScript實(shí)現(xiàn)的選擇排序算法實(shí)例分析
這篇文章主要介紹了JavaScript實(shí)現(xiàn)的選擇排序算法,結(jié)合實(shí)例形式分析了選擇排序的原理、實(shí)現(xiàn)步驟與相關(guān)操作技巧,需要的朋友可以參考下2017-04-04JS co 函數(shù)庫的含義和用法實(shí)例總結(jié)
這篇文章主要介紹了JS co 函數(shù)庫的含義和用法,結(jié)合實(shí)例形式總結(jié)分析了JS co 函數(shù)庫的基本含義、功能、用法及操作注意事項(xiàng),需要的朋友可以參考下2020-04-04JS利用ES6和ES5分別實(shí)現(xiàn)長整數(shù)和字節(jié)數(shù)組互轉(zhuǎn)
這篇文章主要為大家詳細(xì)介紹了長整數(shù)與字節(jié)數(shù)組互轉(zhuǎn)的技術(shù)原理,文中提供了ES6(現(xiàn)代瀏覽器/Node.js)與ES5(兼容舊環(huán)境)兩套實(shí)現(xiàn)方案,需要的可以參考下2025-04-04JS實(shí)現(xiàn)動態(tài)給圖片添加邊框的方法
這篇文章主要介紹了JS實(shí)現(xiàn)動態(tài)給圖片添加邊框的方法,涉及javascript操作圖片border的技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-04-04