JS生態(tài)系統(tǒng)加速桶裝文件使用探索
JS 桶裝文件
長(zhǎng)話短說(shuō):一大坨項(xiàng)目都塞滿了只是重新 export
其他文件的文件。這就是所謂的“桶裝文件”(barrel files),也是 JS 工具在大型項(xiàng)目中慢如龜速的關(guān)鍵原因之一。
本期《前端翻譯計(jì)劃》共享的是“加速 JS 生態(tài)系統(tǒng)系列博客”,包括但不限于:
- PostCSS,SVGO 等等
- 模塊解析
- 使用 eslint
- npm 腳本
- draft-js emoji 插件
- polyfill 暴走
- 桶裝文件暴走
- Tailwind CSS
本期共享的是第 7 篇博客 —— 桶裝文件暴走。
新文件 import
假設(shè)我們正在開(kāi)發(fā)一個(gè)包含一大坨文件的大型項(xiàng)目。我們添加一個(gè)新文件來(lái)處理新功能,并將另一個(gè)目錄中的函數(shù) import
到代碼中。
import { foo } from './some/other-file' export function myCoolCode() { // 假裝這是超智能代碼 :) const result = foo() return result }
我們十分雞凍地實(shí)現(xiàn)了功能,我們運(yùn)行代碼,但是發(fā)現(xiàn)這需要很久才能搞定。我們編寫的代碼一目了然,理論上應(yīng)該不會(huì)太費(fèi)時(shí)。為此,我們添加了某些測(cè)量代碼,查看函數(shù)完成其任務(wù)所需的時(shí)間。
import { foo } from './some/other-file' export function myCoolCode() { console.time() const result = foo() console.timeEnd() return result }
我們重跑代碼,大吃一斤的是,我們插入的測(cè)量結(jié)果表明,我們的函數(shù)速度驚人。我們重復(fù)測(cè)量步驟,但這次在項(xiàng)目的主入口文件中插入 console.time()
語(yǔ)句,然后再重跑代碼。梅開(kāi)二度,記錄的測(cè)量結(jié)果只是反復(fù)證明我們的代碼本身速度驚人。那為什么運(yùn)行代碼卻很費(fèi)時(shí)呢?
沒(méi)時(shí)間解釋了快上車。這就是桶裝文件對(duì)代碼造成的破壞性影響。
收集更多信息
目前為止,我們獲得的關(guān)鍵信息是,代碼的運(yùn)行時(shí)不是 bug 所在。我們測(cè)量了函數(shù),它只是運(yùn)行總時(shí)間的九牛一毛。這意味著,我們可以假設(shè),所有剩余時(shí)間耽誤在運(yùn)行代碼前后。根據(jù)工具的經(jīng)驗(yàn),時(shí)間通?;ㄔ陧?xiàng)目代碼運(yùn)行前。
我有一個(gè)大膽的想法:我們聽(tīng)過(guò)某些 npm 軟件包出于性能考慮,會(huì)預(yù)打包其代碼。也許這是一種新思路?我們決定測(cè)試該理論,并訴諸 esbuild 將代碼打包到一個(gè)簡(jiǎn)單文件中。我們故意禁用任何形式的壓縮,因?yàn)槲覀兿Ma盡可能還原原始源碼。
完事后,我們可以運(yùn)行打包后的文件來(lái)對(duì)照實(shí)驗(yàn),我和我的小伙伴都驚呆了,這次運(yùn)行比貓貓還快。出于好奇,我們測(cè)量了運(yùn)行 esbuild 和一起運(yùn)行打包文件的時(shí)間,并注意到,它們累加起來(lái)仍然比運(yùn)行原始源碼更快。啊?到底是怎么回事?我們明明多了打包步驟,但運(yùn)行結(jié)果卻比未打包的源碼更快?
然后我悟了:打包器的超能力是,拍平和合并模塊圖。得益于 esbuild,曾經(jīng)由數(shù)千個(gè)文件組成的內(nèi)容,被合并為有且僅有一個(gè)簡(jiǎn)單文件。這是一個(gè)有力的證明,模塊圖的大小是源碼運(yùn)行緩慢的 bug 所在。桶裝文件乃速度低下的“萬(wàn)惡之源”。
剖析桶裝文件
所謂“桶裝文件”,指的是僅僅用于 export
其他文件,且本身不包含代碼的文件。在編輯器支持自動(dòng) import
之前,一大坨開(kāi)發(fā)者試圖將它們必須手動(dòng)編寫的 import
語(yǔ)句的數(shù)量保持在最低限度。
// 瞄一眼所有這些 import import { foo } from '../foo' import { bar } from '../bar' import { baz } from '../baz'
這就產(chǎn)生了一種模式,其中每個(gè)文件夾都有自己的 index.js
文件,該文件通常只是從位于同一目錄中的其他文件重新 export
代碼。某種程度上,這分?jǐn)偭耸謩?dòng)輸入工作,因?yàn)橐坏┻@樣的文件就位,所有其他代碼只需要引用一個(gè) import
語(yǔ)句。
// feature/index.js export * from './foo' export * from './bar' export * from './baz'
之前顯示的 import
語(yǔ)句現(xiàn)在可以折疊成一行。
import { foo, bar, baz } from '../feature'
一段時(shí)間后,這種模式在整個(gè)代碼庫(kù)中蔓延,項(xiàng)目中的每個(gè)文件夾都有一個(gè) index.js
文件。十分整潔,對(duì)不?對(duì)對(duì)對(duì),才怪咧。
眾生皆有病
在類似的設(shè)置中,模塊大概率會(huì) import
另一個(gè)桶裝文件,該文件又雙叒叕會(huì)拉入一大坨其他文件,然后 import
另一個(gè)桶裝文件,子子孫孫無(wú)窮盡也。最終,我們通常會(huì)通過(guò) import
語(yǔ)句的“蜘蛛網(wǎng)” import
項(xiàng)目中的每個(gè)文件。項(xiàng)目越大,加載所有這些模塊就越久。
捫心自問(wèn):哪里快了?必須加載 30k 個(gè)文件更快,還是只加載 10 個(gè)更快?
JS 愛(ài)好者的“思想鋼印”在于,模塊只會(huì)按需加載。這大錯(cuò)特錯(cuò),因?yàn)檫@樣做會(huì)破壞依賴全局變量或模塊執(zhí)行順序的代碼。
// a.js globalThis.foo = 123 // b.js console.log(globalThis.foo) // 應(yīng)該打印: 123 // index.js import './a' import './b'
如果引擎無(wú)法加載首個(gè) ./a
導(dǎo)入,那么代碼會(huì)意外打印 undefined
而不是 123
。
桶裝文件的性能瓶頸
當(dāng)我們考慮使用測(cè)試運(yùn)行程序等工具時(shí),情況會(huì)雪上加霜。在人氣爆棚的 Jest 測(cè)試運(yùn)行器中,每個(gè)測(cè)試文件都在其子進(jìn)程中執(zhí)行。實(shí)際上,這意味著,每個(gè)測(cè)試文件都從零開(kāi)始構(gòu)建模塊圖,并且必須支付該成本。如果在項(xiàng)目中構(gòu)建模塊圖需要 6 秒,并且您只有 100 個(gè)測(cè)試文件,那么您總共會(huì)浪費(fèi) 10 分鐘,重復(fù)構(gòu)建模塊圖。在此期間不會(huì)運(yùn)行任何測(cè)試或其他代碼。這正是引擎需要準(zhǔn)備源碼以便運(yùn)行的時(shí)候。
桶裝文件作為“性能殺手”的另一個(gè)法外之地是,任何類型的 import
周期 linting 規(guī)則。通常,linter 按逐個(gè)文件運(yùn)行,這意味著,需要為每個(gè)文件支付構(gòu)建模塊圖的成本。僅此一點(diǎn)就會(huì)導(dǎo)致 linting 時(shí)間爆表,并且在較大的項(xiàng)目中可能需要幾小時(shí),這種情況司空見(jiàn)慣。
為了獲得某些原始數(shù)據(jù),我生成了一個(gè)項(xiàng)目,其中的文件相互 import
,以便更好地了解構(gòu)建模塊圖的成本。每個(gè)文件都空空如也,除了 import
語(yǔ)句外,沒(méi)有包含任何代碼。計(jì)時(shí)是在我的“量子計(jì)算機(jī)”上測(cè)量的。
如你所見(jiàn),加載更少的模塊十分值得。讓我們將這些數(shù)字應(yīng)用到一個(gè)包含 100 個(gè)測(cè)試文件的項(xiàng)目,其中使用測(cè)試運(yùn)行程序?yàn)槊總€(gè)測(cè)試文件生成一個(gè)新的子進(jìn)程。我們的測(cè)試運(yùn)行程序可以并行運(yùn)行 4 個(gè)測(cè)試:
500
個(gè)模塊:0.15s * 100 / 4 = 3.75s
開(kāi)銷
1_000
個(gè)模塊:0.31s * 100 / 4 = 7.75s
開(kāi)銷
25_000
個(gè)模塊:16.81s * 100 / 4 = ~7:00m
開(kāi)銷
50_000
個(gè)模塊:48.44s * 100 / 4 = ~20:00m
開(kāi)銷
由于這是一個(gè)綜合設(shè)置,因此這些數(shù)字都是低估的。在實(shí)際項(xiàng)目中,這些數(shù)字可能更糟糕。就工具性能而言,桶裝文件并不友好。
我們?cè)撜k?
代碼中只有少量桶裝文件通常問(wèn)題不大,但當(dāng)每個(gè)文件夾都有就會(huì)出現(xiàn)問(wèn)題。不幸的是,這種災(zāi)難在 JS 行業(yè)屢見(jiàn)不鮮。
因此,如果大家從事的項(xiàng)目廣泛使用桶裝文件,那么可以免費(fèi)優(yōu)化:清空所有桶裝文件,使一大坨任務(wù)速度加快 60-80%。
免責(zé)聲明
本文屬于是語(yǔ)冰的直男翻譯了屬于是,略有刪改,僅供粉絲參考,英文原味版請(qǐng)傳送 Speeding up the JavaScript ecosystem - The barrel file debacle[1]。
以上就是JS 生態(tài)系統(tǒng)加速桶裝文件使用探索的詳細(xì)內(nèi)容,更多關(guān)于JS 桶裝文件的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- JS生態(tài)系統(tǒng)加速模塊解析賦能性能優(yōu)化探索
- JS生態(tài)系統(tǒng)加速eslint解析器使用實(shí)例探索
- JS生態(tài)系統(tǒng)加速npm腳本優(yōu)化及性能分析探索
- JS生態(tài)系統(tǒng)加速探索Draft-js?emoji插件功能及使用探索
- JS生態(tài)系統(tǒng)加速Tailwind?CSS工作原理探究
- JS?生態(tài)系統(tǒng)加速Polyfill函數(shù)使用實(shí)例探索
- JS生態(tài)系統(tǒng)加速一次一庫(kù)PostCSS SVGO的重構(gòu)源碼和性能優(yōu)化探索
相關(guān)文章
詳解JavaScript實(shí)現(xiàn)繼承的五種經(jīng)典方式(附圖解)
JavaScript中的繼承是一種機(jī)制,通過(guò)它可以創(chuàng)建一個(gè)對(duì)象,該對(duì)象可以享有另一個(gè)對(duì)象的屬性和方法,本文將詳細(xì)的為大家介紹實(shí)現(xiàn)繼承的五種經(jīng)典方式,感興趣的小伙伴跟著小編一起來(lái)看看吧2023-08-08js 鼠標(biāo)移動(dòng)顯示圖片的簡(jiǎn)單實(shí)例
本篇文章主要是對(duì)js鼠標(biāo)移動(dòng)顯示圖片的簡(jiǎn)單實(shí)例進(jìn)行了介紹,需要的朋友可以過(guò)來(lái)參考下,希望對(duì)大家有所幫助2013-12-12suggestion開(kāi)發(fā)小結(jié)以及對(duì)鍵盤事件的總結(jié)(針對(duì)中文輸入法狀態(tài))
suggestion開(kāi)發(fā)小結(jié)以及對(duì)鍵盤事件的總結(jié)(針對(duì)中文輸入法狀態(tài)),需要的朋友可以參考下。2011-12-12JavaScript 刪除或抽取字符串指定字符的方法(極為常用)
這篇文章主要給大家分享了極為常用的JavaScript 刪除或抽取字符串指定字符的所有方法,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2021-12-12js實(shí)現(xiàn)有趣的倒計(jì)時(shí)效果
這篇文章主要為大家詳細(xì)介紹了js實(shí)現(xiàn)有趣的倒計(jì)時(shí)效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-01-01JavaScript通過(guò)join函數(shù)連接數(shù)組里所有元素的方法
這篇文章主要介紹了JavaScript通過(guò)join函數(shù)連接數(shù)組里所有元素的方法,實(shí)例分析了javascript中join函數(shù)的使用技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-03-03