欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

JS生態(tài)系統(tǒng)加速模塊解析賦能性能優(yōu)化探索

 更新時(shí)間:2024年01月21日 14:31:01   作者:大家的林語(yǔ)冰?人貓神話  
這篇文章主要為大家介紹了JS生態(tài)系統(tǒng)加速模塊解析賦能性能優(yōu)化探索,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

引言

長(zhǎng)話短說(shuō):無(wú)論您是在構(gòu)建、測(cè)試或檢查 JS,模塊解析始終是這一切的核心。盡管模塊解析在前端工具鏈中占據(jù)核心地位,但我們并沒(méi)有花太多時(shí)間來(lái)優(yōu)化它。通過(guò)本文討論的變更,工具的速度優(yōu)化 30%。

本期《前端翻譯計(jì)劃》共享的是“加速 JS 生態(tài)系統(tǒng)系列博客”,包括但不限于:

  • PostCSS,SVGO 等等
  • 模塊解析
  • 使用 eslint
  • npm 腳本
  • draft-js emoji 插件
  • polyfill 暴走
  • 桶裝文件崩潰
  • Tailwind CSS

本期共享的是第 2 篇博客 —— 模塊解析賦能性能優(yōu)化。

在本系列的第 1 篇博客中,我們發(fā)現(xiàn)了若干種加速 JS 工具庫(kù)的方法。雖然這些低層補(bǔ)丁使總構(gòu)建時(shí)間大大加速,但我們的工具中是否還有某些基建可以優(yōu)化。那些對(duì)常見(jiàn) JS 任務(wù)(比如打包、測(cè)試和 linting)的總時(shí)間影響更大的東東。

因此,我從我們行業(yè)常用的各種工具中收集了大約十幾個(gè) CPU 配置文件。經(jīng)過(guò)一番檢查后,我發(fā)現(xiàn)每個(gè)配置文件中都存在重復(fù)模式,該模式對(duì)這些任務(wù)的總運(yùn)行時(shí)間的影響高達(dá) 30%。它是我們基建中茲事體大的部分,值得深入探討。

這個(gè)關(guān)鍵部分稱為模塊解析。在我所有調(diào)試中,它花費(fèi)的時(shí)間比解析源碼還要多。

捕獲堆棧跟蹤的成本

當(dāng)我注意到這些跟蹤中最耗時(shí)的方面花費(fèi)在 captureLargerStackTrace 負(fù)責(zé)將堆棧跟蹤附加到 Error 對(duì)象的內(nèi)部 Node 函數(shù)時(shí),好戲就開始了。這似乎有點(diǎn)不同尋常,因?yàn)檫@兩項(xiàng)任務(wù)都成功了,并且沒(méi)有顯示任何拋出錯(cuò)誤的跡象。

單擊分析數(shù)據(jù)中的一系列事件后,可以更清晰地了解正在發(fā)生的情況。幾乎所有錯(cuò)誤的產(chǎn)生都來(lái)自調(diào)用 Node 原生的 fs.statSync() 函數(shù),而該函數(shù)又在名為 isFile 的函數(shù)內(nèi)調(diào)用。文檔提到 fs.statSync() 基本上相當(dāng)于 POSIX 的 fstat 命令,通常用于檢查磁盤上是否存在路徑,是文件還是目錄??紤]到這一點(diǎn),我們應(yīng)該只在文件不存在、缺乏文件讀取權(quán)限或類似特殊用例中獲取錯(cuò)誤。是時(shí)候瞄一下 isFile 的源碼了。

function isFile(file) {
  try {
    const stat = fs.statSync(file)
    return stat.isFile() || stat.isFIFO()
  } catch (err) {
    if (err.code === 'ENOENT' || err.code === 'ENOTDIR') {
      return false
    }
    throw err
  }
}

乍一看,這是一個(gè)看似無(wú)辜的函數(shù),但天網(wǎng)恢恢疏而不漏。值得注意的是,我們忽略某些錯(cuò)誤情況并返回 false,而不是轉(zhuǎn)發(fā)錯(cuò)誤。 ENOENT 和 ENOTDIR 錯(cuò)誤代碼最終都意味著,磁盤上不存在該路徑。也許這就是我們看到的開銷?我的意思是,我們會(huì)立即忽略這些錯(cuò)誤。為了測(cè)試這個(gè)理論,我打印了 try/catch 區(qū)塊捕獲的所有錯(cuò)誤。你瞧,拋出的每個(gè)錯(cuò)誤要么是 ENOENT 代碼,要么是 ENOTDIR 代碼。

查看 Node 的 fs.statSync 文檔可以發(fā)現(xiàn),它支持傳遞 throwIfNoEntry 選項(xiàng),該選項(xiàng)可防止在不存在文件系統(tǒng)入口時(shí)報(bào)錯(cuò)。相反,在那種情況下它將返回 undefined。

function isFile(file) {
  const stat = fs.statSync(file, { throwIfNoEntry: false })
  return stat !== undefined && (stat.isFile() || stat.isFIFO())
}

應(yīng)用此選項(xiàng)允許我們避免 catch 區(qū)塊中的 if 語(yǔ)句,這反過(guò)來(lái)又使 try/catch 變得多余,并允許我們進(jìn)一步簡(jiǎn)化函數(shù)。

這一簡(jiǎn)單更改將項(xiàng)目的 lint 時(shí)間縮短了 7%。更棒的是,測(cè)試也從相同的更改中受益。

文件系統(tǒng)很昂貴

隨著該函數(shù)的堆棧跟蹤開銷被消除,我覺(jué)得還有一大坨優(yōu)化空間。您知道的,在幾分鐘內(nèi)捕獲的跟蹤中根本不應(yīng)該出現(xiàn)幾個(gè)錯(cuò)誤。因此,我在該函數(shù)中注入了一個(gè)簡(jiǎn)單的計(jì)數(shù)器,了解其調(diào)用頻率。顯而易見(jiàn),它被調(diào)用了大約 15k 次,大約是項(xiàng)目中文件數(shù)量的 10 倍。這聽起來(lái)像是一個(gè)優(yōu)化的機(jī)會(huì)。

模塊與否,這是一個(gè)問(wèn)題

默認(rèn)情況下,工具需要了解三種說(shuō)明符:

  • 相對(duì)模塊導(dǎo)入:./foo、../bar/boof

  • 絕對(duì)模塊導(dǎo)入:/foo、/foo/bar/bob

  • 包導(dǎo)入 foo、@foo/bar

從性能角度而言,三者中最有趣的是最后一個(gè)。裸導(dǎo)入說(shuō)明符,即不以點(diǎn) . 或斜杠 / 開頭的導(dǎo)入說(shuō)明符,是一種特殊的導(dǎo)入類型,通常引用 npm 包。該算法在 Node 的文檔中有深入說(shuō)明。其要點(diǎn)是,它嘗試解析包名稱,然后向上遍歷,檢查是否存在包含該模塊的特殊 node_modules 目錄,直到到達(dá)文件系統(tǒng)的根目錄。讓我們用一個(gè)例子來(lái)說(shuō)明一下。

假設(shè)我們有一個(gè)位于 /Users/marvinh/my-project/src/features/DetailPage/components/Layout/index.js 的文件嘗試導(dǎo)入模塊 foo,然后該算法會(huì)檢查以下位置。

/Users/marvinh/my-project/src/features/DetailPage/components/Layout/node_modules/foo/

/Users/marvinh/my-project/src/features/DetailPage/components/node_modules/foo/

/Users/marvinh/my-project/src/features/DetailPage/node_modules/foo/

/Users/marvinh/my-project/src/features/node_modules/foo/

/Users/marvinh/my-project/src/node_modules/foo/

/Users/marvinh/my-project/node_modules/foo/

/Users/marvinh/node_modules/foo/

/Users/node_modules/foo/

此乃一大坨文件系統(tǒng)調(diào)用。簡(jiǎn)而言之,這會(huì)檢查每個(gè)目錄是否包含模塊目錄。檢查數(shù)量與導(dǎo)入文件所在的目錄數(shù)量直接相關(guān)。問(wèn)題是,導(dǎo)入 foo 的每個(gè)文件都會(huì)發(fā)生這種情況。這意味著,如果 foo 被導(dǎo)入到其他地方的文件中,我們會(huì)再次向上爬取整個(gè)目錄樹,直到找到包含該模塊的 node_modules 目錄。這就是緩存已解析模塊有很大幫助的一個(gè)方面。

但它會(huì)變得更好!一大坨項(xiàng)目都使用路徑映射別名來(lái)節(jié)省一點(diǎn)輸入,以便您可以在任何地方使用相同的導(dǎo)入說(shuō)明符,并避免一大坨點(diǎn) ../../../。這通常是通過(guò) TS 的 paths 編譯器選項(xiàng)或打包器中的解析別名來(lái)完成的。問(wèn)題在于,這些通常與包導(dǎo)入無(wú)法區(qū)分。如果我在 /Users/marvinh/my-project/src/features/ 處添加到功能目錄的路徑映射,以便我可以使用諸如 import {...} from “features/DetailPage” 之類的導(dǎo)入聲明,那么每個(gè)工具都應(yīng)該知道這一點(diǎn)。

但如果沒(méi)有呢?由于沒(méi)有每個(gè) JS 工具都使用的集中模塊解析包,因此它們是多個(gè)相互競(jìng)爭(zhēng)的工具,支持不同級(jí)別的功能。就本人而言,該項(xiàng)目大量使用路徑映射,并且它包含一個(gè) linting 插件,該插件不知道 TS 的 tsconfig.json 中定義的路徑映射。自然地,它假設(shè) features/DetailPage 指的是一個(gè) Node 模塊,這導(dǎo)致它執(zhí)行整個(gè)遞歸向上遍歷,希望找到該模塊。但它從未這樣做過(guò),所以它會(huì)報(bào)錯(cuò)。

緩存所有的東西

接下來(lái),我增強(qiáng)了日志記錄,查看調(diào)用該函數(shù)的唯一文件路徑有多少個(gè),以及它是否始終返回相同的結(jié)果。只有大約 2.5k 個(gè)對(duì) isFile 的調(diào)用具有唯一的文件路徑,并且傳遞的文件參數(shù)和返回值之間存在強(qiáng)大的 1:1 映射。它仍然比項(xiàng)目中的文件數(shù)量多,但比總共被調(diào)用的 15k 次要低得多。如果我們?cè)谄渲車砑右粋€(gè)緩存,避免訪問(wèn)文件系統(tǒng),那會(huì)如何?

const cache = new Map()

function resolve(file) {
  const cached = cache.get(file)
  if (cached !== undefined) return cached

  // 這里存在解析邏輯......

  const resolved = isFile(file)
  cache.set(file, resolved)
  return file
}

添加緩存使總 linting 時(shí)間又加快了 15%。不過(guò),緩存的風(fēng)險(xiǎn)在于,它們可能會(huì)變得過(guò)時(shí)。它們通常必須在某個(gè)時(shí)間點(diǎn)失效。為了安全起見(jiàn),我最終選擇了一種更保守的方法來(lái)檢查緩存的文件是否仍然存在。如果您認(rèn)為工具經(jīng)常在監(jiān)視模式下運(yùn)行,那這種情況并不罕見(jiàn),在監(jiān)視模式下,期望盡可能多地緩存,并且僅使更改的文件無(wú)效。

const cache = new Map()
function resolve(file) {
  const cached = cache.get(file)
  // 保守策略:檢查緩存文件是否存在硬盤上
  // 避免監(jiān)測(cè)模式下穩(wěn)定緩存時(shí)文件可能移動(dòng)或重命名
  if (cached !== undefined && isFile(file)) {
    return cached
  }
  // 這里存在解析邏輯......
  for (const ext of extensions) {
    const filePath = file + ext
    if (isFile(filePath)) {
      cache.set(file, filePath)
      return filePath
    }
  }
  throw new Error(`Could not resolve ${file}`)
}

老實(shí)說(shuō),我本來(lái)希望它首先會(huì)抵消添加緩存的好處,因?yàn)榧词乖诰彺鎴?chǎng)景中我們也會(huì)訪問(wèn)文件系統(tǒng)。但從數(shù)字來(lái)看,這只使總 linting 時(shí)間惡化了 0.05%。相比之下,這是一個(gè)非常小的影響,但額外的文件系統(tǒng)調(diào)用不是更重要嗎?

文件擴(kuò)展名猜謎游戲

JS 中的模塊問(wèn)題在于,該語(yǔ)言從一開始就沒(méi)有模塊系統(tǒng)。當(dāng) Node 橫空出世時(shí),它普及了 CommonJS 模塊系統(tǒng)。該系統(tǒng)有若干“可愛(ài)”的功能,比如能夠省略正在加載的文件的擴(kuò)展名。當(dāng)您編寫諸如 require("./foo") 之類的語(yǔ)句時(shí),它會(huì)自動(dòng)添加 .js 擴(kuò)展名,并嘗試讀取 ./foo.js 處的文件。如果不存在,它將檢查 json 文件 ./foo.json ,如果也不可用,它將檢查 ./foo/index.js 處的 index 文件。

實(shí)際上,我們?cè)谶@里處理的是歧義,工具必須能夠理解 ./foo 的解析結(jié)果。這樣,很可能會(huì)產(chǎn)生浪費(fèi)的文件系統(tǒng)調(diào)用,因?yàn)闊o(wú)法提前知道將文件解析到哪里。工具實(shí)際上必須嘗試每種組合,直到找到匹配項(xiàng)。如果我們看看目前存在的可能擴(kuò)展的總數(shù),情況會(huì)變得更糟。工具通常有一系列潛在的擴(kuò)展需要檢查。如果您包含 TS,那么在撰寫本文時(shí),典型前端項(xiàng)目的完整列表為:

const extensions = [
  '.js',
  '.jsx',
  '.cjs',
  '.mjs',
  '.ts',
  '.tsx',
  '.mts',
  '.cts'
]

有 8 個(gè)需要檢查的潛在擴(kuò)展。這還不是全部。您實(shí)際上必須將該列表加倍才能考慮 index 文件,這些文件也可以解析為所有這些擴(kuò)展名!這意味著,我們的工具除了循環(huán)瀏覽擴(kuò)展列表,直到找到磁盤上存在的擴(kuò)展之外,沒(méi)有其他選擇。當(dāng)我們想要解析 ./foo,并且實(shí)際文件是 foo.ts 時(shí),我們需要檢查:

foo.js -> 不存在

foo.jsx -> 不存在

foo.cjs -> 不存在

foo.mjs -> 不存在

foo.ts -> 終于找到了!

這是四個(gè)不必要的文件系統(tǒng)調(diào)用。當(dāng)然,您可以更改擴(kuò)展的順序,并將項(xiàng)目中最常見(jiàn)的擴(kuò)展放在數(shù)組的開頭。這會(huì)增加提前找到正確擴(kuò)展的機(jī)會(huì),但并不能完全消除問(wèn)題。

作為 ES2015 規(guī)范的一部分,提出了一個(gè)新的模塊系統(tǒng)。所有細(xì)節(jié)都沒(méi)有及時(shí)充實(shí),但語(yǔ)法卻充實(shí)了。import 語(yǔ)句很快就占據(jù)了主導(dǎo)地位,因?yàn)樗鼈冊(cè)诠ぞ叻矫姹?CommonJS 有優(yōu)勢(shì)。由于其靜態(tài)性,它為更多工具增強(qiáng)功能開辟了空間,比如最著名的 tree-shaking(樹搖優(yōu)化),其中未使用的模塊甚至模塊中的功能可以輕松檢測(cè)到,并從生產(chǎn)版本中刪除。自然而然地,每個(gè)人都接受了新的導(dǎo)入語(yǔ)法。

但有一個(gè)問(wèn)題:只最終確定了語(yǔ)法,而不是實(shí)際的模塊加載或解析應(yīng)該如何工作。為了填補(bǔ)這一空白,工具重新使用了 CommonJS 中的現(xiàn)有語(yǔ)義。這對(duì)于采用是有好處的,因?yàn)橐浦泊蠖鄶?shù)代碼庫(kù)只需要進(jìn)行語(yǔ)法更改,并且這些可以通過(guò) codemods 自動(dòng)化。從采用的角度來(lái)看,這是一個(gè)很棒的方面!但這也意味著,我們繼承了導(dǎo)入說(shuō)明符應(yīng)解析為哪個(gè)文件擴(kuò)展名的猜測(cè)游戲。

模塊加載和解析的實(shí)際規(guī)范在幾年后最終確定,并通過(guò)強(qiáng)制擴(kuò)展糾正了這個(gè)錯(cuò)誤。

// 非法 ESM,導(dǎo)入說(shuō)明符缺失擴(kuò)展名
import { doSomething } from './foo'

// 合法 ESM
import { doSomething } from './foo.js'

通過(guò)消除這種歧義源并始終添加擴(kuò)展,我們可以避免一整類問(wèn)題。工具也變得更快。但生態(tài)系統(tǒng)在這方面取得進(jìn)展(甚至根本沒(méi)有取得進(jìn)展)還需要時(shí)間,因?yàn)楣ぞ咭呀?jīng)適應(yīng)了處理模糊性的問(wèn)題。

路在何方?

在整個(gè)調(diào)查過(guò)程中,我有點(diǎn)驚訝地發(fā)現(xiàn)在優(yōu)化模塊分辨率方面還有一大坨優(yōu)化空間,因?yàn)樗俏覀児ぞ叩暮诵摹1疚闹忻枋龅娜舾筛膶?linting 時(shí)間減少了 30%!

我們?cè)谶@里所做的若干優(yōu)化也不是 JS 獨(dú)有的。這些優(yōu)化與其他編程語(yǔ)言的工具中可以找到的優(yōu)化相同。當(dāng)談到模塊分辨率時(shí),四個(gè)主要要點(diǎn)是:

  • 盡可能避免調(diào)用文件系統(tǒng)
  • 盡可能緩存,避免調(diào)用文件系統(tǒng)
  • 當(dāng)使用 fs.stat 或 fs.statSync 時(shí),請(qǐng)始終設(shè)置 throwIfNoEntry: false
  • 盡可能限制向上遍歷

我們工具的緩慢并不是由 JS 這種語(yǔ)言造成的,而是因?yàn)楦緵](méi)有優(yōu)化。JS 生態(tài)系統(tǒng)的碎片化也無(wú)濟(jì)于事,因?yàn)闆](méi)有一個(gè)用于模塊解析的標(biāo)準(zhǔn)包。相反,有很多個(gè)包,并且它們都共享不同的功能子集。這并不奇怪,因?yàn)槎嗄陙?lái)支持的功能列表不斷增長(zhǎng),并且在撰寫本文時(shí)還沒(méi)有一個(gè)庫(kù)可以支持所有這些功能。擁有一個(gè)每個(gè)人都使用的單一庫(kù)能使每個(gè)人一勞永逸地解決此問(wèn)題。

免責(zé)聲明

本文屬于是語(yǔ)冰的直男翻譯了屬于是,略有刪改,僅供粉絲參考,英文原味版請(qǐng)傳送 Speeding up the JavaScript ecosystem - module resolution

以上就是JS生態(tài)系統(tǒng)加速模塊解析賦能性能優(yōu)化探索的詳細(xì)內(nèi)容,更多關(guān)于JS模塊解析的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • javascript實(shí)現(xiàn)移動(dòng)端紅包雨頁(yè)面

    javascript實(shí)現(xiàn)移動(dòng)端紅包雨頁(yè)面

    這篇文章主要為大家詳細(xì)介紹了javascript實(shí)現(xiàn)移動(dòng)端紅包雨頁(yè)面,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2020-06-06
  • Underscore源碼分析

    Underscore源碼分析

    Underscore 是一個(gè) JavaScript 工具庫(kù),它提供了一整套函數(shù)式編程的實(shí)用功能,但是沒(méi)有擴(kuò)展任何 JavaScript 內(nèi)置對(duì)象。這篇文章主要介紹了underscore源碼分析相關(guān)知識(shí),感興趣的朋友一起學(xué)習(xí)吧
    2015-12-12
  • javascript鍵盤上下鍵的操作(選擇)

    javascript鍵盤上下鍵的操作(選擇)

    不錯(cuò)的使用鍵盤上下鍵實(shí)現(xiàn)選擇的代碼,方便用戶操作
    2008-06-06
  • JavaScript實(shí)現(xiàn)垂直滾動(dòng)條效果

    JavaScript實(shí)現(xiàn)垂直滾動(dòng)條效果

    這篇文章為大家詳細(xì)主要介紹了JavaScript實(shí)現(xiàn)垂直滾動(dòng)條效果的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2017-01-01
  • JavaScript中的作用域與閉包、原型與原型鏈、異步與單線程

    JavaScript中的作用域與閉包、原型與原型鏈、異步與單線程

    JavaScript的三座大山指的是:作用域和閉包、原型和原型鏈、異步與單線程,這些概念在日常的開發(fā)工作中經(jīng)常被提及,并對(duì)我們理解和編寫高質(zhì)量的JavaScript代碼至關(guān)重要
    2024-02-02
  • 多次注冊(cè)事件會(huì)導(dǎo)致一個(gè)事件被觸發(fā)多次的解決方法

    多次注冊(cè)事件會(huì)導(dǎo)致一個(gè)事件被觸發(fā)多次的解決方法

    一個(gè)JavaScript邏輯,會(huì)自動(dòng)綁定函數(shù)到按鈕的click事件,但是這段代碼會(huì)反復(fù)注冊(cè)事件,具體的解決方法如下,感興趣的朋友可以參考下
    2013-08-08
  • es6中some和every方法使用簡(jiǎn)單示例

    es6中some和every方法使用簡(jiǎn)單示例

    JavaScript在ES6版本后提供了一些更加便捷的方法供開發(fā)者使用,實(shí)現(xiàn)原理其實(shí)是在對(duì)應(yīng)的構(gòu)造函數(shù)原型提供方法,下面這篇文章主要給大家介紹了關(guān)于es6中some和every方法使用的相關(guān)資料,需要的朋友可以參考下
    2023-04-04
  • js將列表組裝成樹結(jié)構(gòu)的兩種實(shí)現(xiàn)方式分享

    js將列表組裝成樹結(jié)構(gòu)的兩種實(shí)現(xiàn)方式分享

    最近做的任務(wù)提了新的需求,需要實(shí)現(xiàn)一個(gè)樹形結(jié)構(gòu),所以下面這篇文章主要給大家介紹了關(guān)于js將列表組裝成樹結(jié)構(gòu)的兩種實(shí)現(xiàn)方式,需要的朋友可以參考下
    2022-01-01
  • JS中用try catch對(duì)代碼運(yùn)行的性能影響分析

    JS中用try catch對(duì)代碼運(yùn)行的性能影響分析

    要捕獲JavaScript代碼中的異常一般會(huì)采用 try catch,不過(guò)try catch的使用是否是對(duì)代碼性能產(chǎn)生影響呢?答案是肯定有的,但是有多少不得而知。下面這篇文章就給大家詳細(xì)介紹了在JS中用try catch對(duì)代碼運(yùn)行的性能影響,有需要的朋友們可以參考借鑒。
    2016-12-12
  • JS使用Expires?max-age判斷緩存過(guò)期的瀏覽器實(shí)例

    JS使用Expires?max-age判斷緩存過(guò)期的瀏覽器實(shí)例

    這篇文章主要為大家介紹了JS使用Expires?max-age判斷緩存過(guò)期的瀏覽器實(shí)例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-11-11

最新評(píng)論