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