JS生態(tài)系統(tǒng)加速桶裝文件使用探索
JS 桶裝文件
長話短說:一大坨項(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è)我們正在開發(fā)一個(gè)包含一大坨文件的大型項(xiàng)目。我們添加一個(gè)新文件來處理新功能,并將另一個(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)該不會太費(fèi)時(shí)。為此,我們添加了某些測量代碼,查看函數(shù)完成其任務(wù)所需的時(shí)間。
import { foo } from './some/other-file'
export function myCoolCode() {
console.time()
const result = foo()
console.timeEnd()
return result
}
我們重跑代碼,大吃一斤的是,我們插入的測量結(jié)果表明,我們的函數(shù)速度驚人。我們重復(fù)測量步驟,但這次在項(xiàng)目的主入口文件中插入 console.time() 語句,然后再重跑代碼。梅開二度,記錄的測量結(jié)果只是反復(fù)證明我們的代碼本身速度驚人。那為什么運(yùn)行代碼卻很費(fèi)時(shí)呢?
沒時(shí)間解釋了快上車。這就是桶裝文件對代碼造成的破壞性影響。
收集更多信息
目前為止,我們獲得的關(guān)鍵信息是,代碼的運(yùn)行時(shí)不是 bug 所在。我們測量了函數(shù),它只是運(yùn)行總時(shí)間的九牛一毛。這意味著,我們可以假設(shè),所有剩余時(shí)間耽誤在運(yùn)行代碼前后。根據(jù)工具的經(jīng)驗(yàn),時(shí)間通?;ㄔ陧?xiàng)目代碼運(yùn)行前。
我有一個(gè)大膽的想法:我們聽過某些 npm 軟件包出于性能考慮,會預(yù)打包其代碼。也許這是一種新思路?我們決定測試該理論,并訴諸 esbuild 將代碼打包到一個(gè)簡單文件中。我們故意禁用任何形式的壓縮,因?yàn)槲覀兿Ma盡可能還原原始源碼。
完事后,我們可以運(yùn)行打包后的文件來對照實(shí)驗(yàn),我和我的小伙伴都驚呆了,這次運(yùn)行比貓貓還快。出于好奇,我們測量了運(yùn)行 esbuild 和一起運(yùn)行打包文件的時(shí)間,并注意到,它們累加起來仍然比運(yùn)行原始源碼更快。?。康降资窃趺椿厥??我們明明多了打包步驟,但運(yùn)行結(jié)果卻比未打包的源碼更快?
然后我悟了:打包器的超能力是,拍平和合并模塊圖。得益于 esbuild,曾經(jīng)由數(shù)千個(gè)文件組成的內(nèi)容,被合并為有且僅有一個(gè)簡單文件。這是一個(gè)有力的證明,模塊圖的大小是源碼運(yùn)行緩慢的 bug 所在。桶裝文件乃速度低下的“萬惡之源”。
剖析桶裝文件
所謂“桶裝文件”,指的是僅僅用于 export 其他文件,且本身不包含代碼的文件。在編輯器支持自動(dòng) import 之前,一大坨開發(fā)者試圖將它們必須手動(dòng)編寫的 import 語句的數(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 語句。
// feature/index.js export * from './foo' export * from './bar' export * from './baz'
之前顯示的 import 語句現(xiàn)在可以折疊成一行。
import { foo, bar, baz } from '../feature'
一段時(shí)間后,這種模式在整個(gè)代碼庫中蔓延,項(xiàng)目中的每個(gè)文件夾都有一個(gè) index.js 文件。十分整潔,對不?對對對,才怪咧。
眾生皆有病
在類似的設(shè)置中,模塊大概率會 import 另一個(gè)桶裝文件,該文件又雙叒叕會拉入一大坨其他文件,然后 import 另一個(gè)桶裝文件,子子孫孫無窮盡也。最終,我們通常會通過 import 語句的“蜘蛛網(wǎng)” import 項(xiàng)目中的每個(gè)文件。項(xiàng)目越大,加載所有這些模塊就越久。
捫心自問:哪里快了?必須加載 30k 個(gè)文件更快,還是只加載 10 個(gè)更快?
JS 愛好者的“思想鋼印”在于,模塊只會按需加載。這大錯(cuò)特錯(cuò),因?yàn)檫@樣做會破壞依賴全局變量或模塊執(zhí)行順序的代碼。
// a.js globalThis.foo = 123 // b.js console.log(globalThis.foo) // 應(yīng)該打印: 123 // index.js import './a' import './b'
如果引擎無法加載首個(gè) ./a 導(dǎo)入,那么代碼會意外打印 undefined 而不是 123。
桶裝文件的性能瓶頸
當(dāng)我們考慮使用測試運(yùn)行程序等工具時(shí),情況會雪上加霜。在人氣爆棚的 Jest 測試運(yùn)行器中,每個(gè)測試文件都在其子進(jìn)程中執(zhí)行。實(shí)際上,這意味著,每個(gè)測試文件都從零開始構(gòu)建模塊圖,并且必須支付該成本。如果在項(xiàng)目中構(gòu)建模塊圖需要 6 秒,并且您只有 100 個(gè)測試文件,那么您總共會浪費(fèi) 10 分鐘,重復(fù)構(gòu)建模塊圖。在此期間不會運(yùn)行任何測試或其他代碼。這正是引擎需要準(zhǔn)備源碼以便運(yùn)行的時(shí)候。
桶裝文件作為“性能殺手”的另一個(gè)法外之地是,任何類型的 import 周期 linting 規(guī)則。通常,linter 按逐個(gè)文件運(yùn)行,這意味著,需要為每個(gè)文件支付構(gòu)建模塊圖的成本。僅此一點(diǎn)就會導(dǎo)致 linting 時(shí)間爆表,并且在較大的項(xiàng)目中可能需要幾小時(shí),這種情況司空見慣。
為了獲得某些原始數(shù)據(jù),我生成了一個(gè)項(xiàng)目,其中的文件相互 import,以便更好地了解構(gòu)建模塊圖的成本。每個(gè)文件都空空如也,除了 import 語句外,沒有包含任何代碼。計(jì)時(shí)是在我的“量子計(jì)算機(jī)”上測量的。

如你所見,加載更少的模塊十分值得。讓我們將這些數(shù)字應(yīng)用到一個(gè)包含 100 個(gè)測試文件的項(xiàng)目,其中使用測試運(yùn)行程序?yàn)槊總€(gè)測試文件生成一個(gè)新的子進(jìn)程。我們的測試運(yùn)行程序可以并行運(yùn)行 4 個(gè)測試:
500 個(gè)模塊:0.15s * 100 / 4 = 3.75s 開銷
1_000 個(gè)模塊:0.31s * 100 / 4 = 7.75s 開銷
25_000 個(gè)模塊:16.81s * 100 / 4 = ~7:00m 開銷
50_000 個(gè)模塊:48.44s * 100 / 4 = ~20:00m 開銷
由于這是一個(gè)綜合設(shè)置,因此這些數(shù)字都是低估的。在實(shí)際項(xiàng)目中,這些數(shù)字可能更糟糕。就工具性能而言,桶裝文件并不友好。
我們該咋辦?
代碼中只有少量桶裝文件通常問題不大,但當(dāng)每個(gè)文件夾都有就會出現(xiàn)問題。不幸的是,這種災(zāi)難在 JS 行業(yè)屢見不鮮。
因此,如果大家從事的項(xiàng)目廣泛使用桶裝文件,那么可以免費(fèi)優(yōu)化:清空所有桶裝文件,使一大坨任務(wù)速度加快 60-80%。
免責(zé)聲明
本文屬于是語冰的直男翻譯了屬于是,略有刪改,僅供粉絲參考,英文原味版請傳送 Speeding up the JavaScript ecosystem - The barrel file debacle[1]。
以上就是JS 生態(tài)系統(tǒng)加速桶裝文件使用探索的詳細(xì)內(nèi)容,更多關(guān)于JS 桶裝文件的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
js判斷數(shù)組是否包含某個(gè)字符串變量的實(shí)例
下面小編就為大家分享一篇js判斷數(shù)組是否包含某個(gè)字符串變量的實(shí)例,具有很好的參考價(jià)值,希望對大家有所幫助2017-11-11
微信小程序?qū)崿F(xiàn)一個(gè)自定義遮罩層效果
這篇文章主要介紹了微信小程序?qū)崿F(xiàn)一個(gè)自定義遮罩層,大概效果是點(diǎn)擊按鈕Show顯示遮罩層,再次點(diǎn)擊屏幕任何地方隱藏遮罩層,本文結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2023-09-09
JavaScript SHA-256加密算法詳細(xì)代碼
這篇文章主要為大家詳細(xì)介紹了JavaScript SHA-256加密算法代碼,具有一定的參考價(jià)值,感興趣的朋友可以參考一下2016-10-10
在JavaScript中使用Promise.allSettled()的方法
Promise.allSettled()是一個(gè)游戲規(guī)則改變者,讓您等待所有承諾得到解決(解決或拒絕),然后根據(jù)結(jié)果采取行動(dòng),這篇文章主要介紹了如何在JavaScript中使用Promise.allSettled(),需要的朋友可以參考下2023-07-07
JavaScript通過IP地址獲取用戶精確位置的代碼實(shí)現(xiàn)
在Web開發(fā)中,獲取用戶地理位置是常見的需求,傳統(tǒng)的HTML5 Geolocation API雖然精確,但需要用戶授權(quán),且移動(dòng)端支持較好而桌面端較差,本文將介紹JavaScript如何通過IP地址獲取用戶精確位置,需要的朋友可以參考下2026-01-01
JavaScript垃圾回收機(jī)制原理總結(jié)深入探究
就像人類會產(chǎn)生垃圾一樣,程序運(yùn)行過程中也會產(chǎn)生垃圾,如果不及時(shí)回收輕則將會拖慢程序運(yùn)行,重則會導(dǎo)致系統(tǒng)崩潰,也就是所謂的內(nèi)存泄漏。所以垃圾回收非常必要2022-10-10
layui-laydate時(shí)間日歷控件使用方法詳解
這篇文章主要為大家詳細(xì)介紹了layui-laydate時(shí)間日歷控件的使用方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-11-11

