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

JS生態(tài)系統(tǒng)加速npm腳本優(yōu)化及性能分析探索

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

引言

長(zhǎng)話短說(shuō):npm 腳本總是由整顆地球的 JS 開(kāi)發(fā)者和 CI(持續(xù)集成)系統(tǒng)執(zhí)行。盡管使用率很高,但它們并沒(méi)有得到良好優(yōu)化,且增加了大約 400 毫秒的開(kāi)銷。在本文中,我們能夠?qū)⑵鋬?yōu)化至約 22 毫秒。

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

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

npm 腳本

本期共享的是第 4 篇博客 —— npm 腳本。

如果使用 JS,您可能使用過(guò) package.json 中的 "scripts" 字段,為項(xiàng)目設(shè)置常見(jiàn)任務(wù)。這些腳本可以在終端上使用 npm run 執(zhí)行。我傾向于直接調(diào)用底層命令,而不是調(diào)用 npm run,主要因?yàn)檫@明顯更快。但反而言之,是什么讓它們慢如龜速呢?是時(shí)候進(jìn)行性能分析了!

僅按需加載加載代碼

一大坨開(kāi)發(fā)者不知道的是,npm CLI 是一個(gè)標(biāo)準(zhǔn) JS 文件,可以像其他 .js 文件一樣執(zhí)行。在 macOS 和 Linux 上,您可以通過(guò)運(yùn)行 which npm 獲取 npm cli 的完整路徑。將該文件轉(zhuǎn)儲(chǔ)到終端表明,它是一個(gè)平平無(wú)奇的標(biāo)準(zhǔn) .js 文件。唯一奇葩在于首行代碼,它告訴 shell 可以使用哪個(gè)程序來(lái)執(zhí)行當(dāng)前文件。因?yàn)槲覀冋谔幚硪粋€(gè) node 的 JS 文件。

因?yàn)樗皇且粋€(gè) .js 文件,所以我們可以依靠所有常用方法來(lái)生成配置文件。我最喜歡的是 Node 的 --cpu-prof 參數(shù)。將這些知識(shí)結(jié)合在一起,我們可以通過(guò) node --cpu-prof $(which npm) run myscript,從 npm 腳本生成配置文件。將該配置文件加載到 speedscope 中,可以揭示一大坨有關(guān) npm 結(jié)構(gòu)的信息。

大部分時(shí)間都花在加載構(gòu)成 npm cli 的所有模塊上。相比之下,我們運(yùn)行的腳本的時(shí)間就相形見(jiàn)絀了。我們看到一大坨文件,似乎只有在滿足特定條件時(shí)才需要。舉個(gè)栗子,格式化錯(cuò)誤消息的代碼,當(dāng)且僅當(dāng)發(fā)生錯(cuò)誤時(shí)才需要。

npm 中存在這種情況,exit 句柄無(wú)腦 require。讓我們當(dāng)且僅當(dāng)需要時(shí),才 require 該模塊。

  // exit-handler.js
  const log = require('./log-shim.js')
- const errorMessage = require('./error-message.js')
- const replaceInfo = require('./replace-info.js')

  const exitHandler = err => {
    //...
    if (err) {
+     const replaceInfo = require('./replace-info.js');
+     const errorMessage = require('./error-message.js')
      //...
    }
  };

將更改后與未更改的配置文件比較,不會(huì)顯示總時(shí)間存在差異。這是因?yàn)槲覀冊(cè)谶@里更改為延遲加載的模塊在其他地方餓漢式 require。為了正確地延遲加載它們,我們需要更改所有 require 的地方。

接下來(lái)我注意到,加載了一堆與 npm 審計(jì)功能相關(guān)的代碼。這看起來(lái)很奇葩,因?yàn)槲覜](méi)有運(yùn)行任何審計(jì)相關(guān)的東東。不幸的是,對(duì)我們而言,這并不像移動(dòng)某些 require 調(diào)用那么容易。

萬(wàn)能類

各種 JS 工具中反復(fù)出現(xiàn)的一個(gè)問(wèn)題是,它們由一大坨類組成,這些類包含所有內(nèi)容,而不僅僅是我們需要的代碼。這些類總是從小規(guī)模開(kāi)始,并有良好的精簡(jiǎn)意圖,但不知何故,它們變得越來(lái)越腫。確保按需加載代碼越來(lái)越難。這讓我想起 Joe Armstrong(Erlang 之父)的這句名言:

“您只想要一根香蕉,但您得到的是一只大猩猩拿著香蕉和整個(gè)叢林。”

npm 內(nèi)部有一個(gè) Arborist 類,它引入了一大坨僅特定命令所需的東東。它引入了與修改 node_modules 中的布局和包、審核包版本以及 npm run 命令不需要的其他一大坨相關(guān)內(nèi)容。如果我們想優(yōu)化 npm run,我們需要將它們從無(wú)腦加載的模塊列表中剔除。

const mixins = [
  require('../tracker.js'),
  require('./pruner.js'),
  require('./deduper.js'),
  require('./audit.js'),
  require('./build-ideal-tree.js'),
  require('./load-workspaces.js'),
  require('./load-actual.js'),
  require('./load-virtual.js'),
  require('./rebuild.js'),
  require('./reify.js'),
  require('./isolated-reifier.js')
]

const Base = mixins.reduce((a, b) => b(a), require('events'))
class Arborist extends Base {
  //...
}

出于我們的目的,所有加載到 mixins 數(shù)組中的模塊(Arborist 類稍后在其上擴(kuò)展)都不需要。我們可以一鍵清空回收站。這一更改優(yōu)化了大約 20 毫秒,這可能看似九牛一毛,但積少成多。和以前一樣,我們需要檢查 require 這些模塊的其他地方,確保我們確實(shí)只按需加載它。

減小模塊圖大小

對(duì)隨處可見(jiàn)的一大坨 require 語(yǔ)句進(jìn)行更改很好,但不會(huì)顯著影響性能。更大的問(wèn)題在于依賴,它通常有一個(gè)主入口文件,該文件提取所述模塊的所有代碼。最終問(wèn)題在于,當(dāng)引擎瞄到一大坨頂層 import 或 require 語(yǔ)句時(shí),它會(huì)餓漢式解析并加載這些模塊。無(wú)一例外。但這正是我們想要避免的。

一個(gè)具體的例子是,從 npm-registry-fetch 包導(dǎo)入的 cleanUrl 函數(shù)。顧名思義,該包主要關(guān)于網(wǎng)絡(luò)方面。但運(yùn)行腳本時(shí),我們不會(huì)在 npm run 中執(zhí)行任何類型的網(wǎng)絡(luò)請(qǐng)求。這又優(yōu)化了 20 毫秒。我們也不需要顯示進(jìn)度條,因此我們也可以刪除其代碼。npm cli 使用的一大坨其他依賴也是舉一反一。

對(duì)于這些場(chǎng)景而言,加載的模塊數(shù)量是一個(gè)非常現(xiàn)實(shí)的問(wèn)題。見(jiàn)怪不怪,對(duì)于啟動(dòng)時(shí)間茲事體大的庫(kù)已轉(zhuǎn)向打包器,將其所有代碼合并到更少的文件中。引擎非常適合加載 JS 大型 blob。我們?nèi)绱岁P(guān)心網(wǎng)絡(luò)上文件大小的主要原因在于,通過(guò)網(wǎng)絡(luò)傳輸那些字節(jié)的成本。

不過(guò),此方案也有權(quán)衡。文件越大,解析時(shí)間就越長(zhǎng),因此存在有一個(gè)閾值,超過(guò)該閾值后,單個(gè)大文件的解析成本會(huì)高于將其拆分。與往常一樣:測(cè)量將告訴,您是否達(dá)到了這種均衡。另一件需要考慮的事情是,打包器無(wú)法像 ESM 代碼那樣高效地打包 CommonJS 模塊系統(tǒng)的代碼。通常,它們會(huì)在 CommonJS 模塊周圍引入一大坨包裝代碼,這首先抵消了打包代碼的大部分福利。

排序所有字符串

隨著模塊圖的逐次遞減,配置文件的干擾越來(lái)越小,并揭露了其他可以優(yōu)化的地方。對(duì) collaterCompare 函數(shù)的特定調(diào)用引起了我的注意。

您可能會(huì)認(rèn)為,10 毫秒的優(yōu)化性價(jià)比太低,但在此配置文件中,它更像是“勿以善小而不為”。沒(méi)有任何銀彈可以讓一切加速。因此,優(yōu)化小型的調(diào)用位置非常值得。collatorCompare 函數(shù)的有趣之處在于,其預(yù)期目的是以區(qū)域設(shè)置感知的方式排序字符串。該實(shí)現(xiàn)分為兩部分:初始化函數(shù)及其返回的實(shí)際比較的函數(shù)。

// @isaacs/string-locale-compare 中代碼的簡(jiǎn)化示例
const collatorCompare = (locale, opts) => {
  const collator = new Intl.Collator(locale, opts)
  // 始終返回一個(gè)需要從零開(kāi)始優(yōu)化的函數(shù)
  return (a, b) => collator.compare(a, b)
}
const cache = new Map()
module.exports = (locale, options = {}) => {
  const key = `${locale}\n${JSON.stringify(options)}`
  if (cache.has(key)) return cache.get(key)
  const compare = collatorCompare(locale, opts)
  cache.set(key, compare)
  return compare
}

如果我們查看該模塊加載的所有位置,可以看到我們只對(duì)排序英文字符串感興趣,并且從不傳遞除語(yǔ)言環(huán)境之外的任何其他選項(xiàng)。但由于該模塊的結(jié)構(gòu)化方式,每個(gè)新的 require 調(diào)用都會(huì)促使我們創(chuàng)建一個(gè)需要再次優(yōu)化的全新比較函數(shù)。

// 每個(gè) require 調(diào)用立即使用 en 調(diào)用默認(rèn)導(dǎo)出
const localeCompare = require('@isaacs/string-locale-compare')('en')

但理想情況下,我們希望大家都使用相同的比較函數(shù)??紤]到這一點(diǎn),我們可以用兩行代碼替換,其中我們創(chuàng)建了一次 Intl.Collator,并且也只創(chuàng)建一次 localeCompare 函數(shù)。

// 我們只需構(gòu)造一次 Collator 類的實(shí)例
const collator = new Intl.Collator('en')
const localeCompare = (a, b) => collator.compare(a, b)

在某個(gè)特定位置,npm 保存可用命令的排序列表。該列表是硬編碼的,并且在運(yùn)行時(shí)永遠(yuǎn)不變。它僅由 ascii 字符串組成,因此我們可以使用普通的原有 .sort(),而不是我們的區(qū)域設(shè)置感知函數(shù)。

  // 此數(shù)組僅包含 ASCII 字符串
  const commands = [
    'access',
    'adduser',
    'audit',
    'bugs',
    'cache',
    'ci',
    // ...
- ].sort(localeCompare)
+ ].sort()

通過(guò)此優(yōu)化,調(diào)用該函數(shù)的時(shí)間趨近 0 毫秒。這又優(yōu)化了 10 毫秒,因?yàn)榇四俗詈笠粋€(gè)餓漢式加載該模塊的地方。

粉絲請(qǐng)注意,此時(shí)我們已經(jīng)將 npm run 的速度提高了一倍。我們現(xiàn)在從開(kāi)始時(shí)的約 400 毫秒減少到約 200 毫秒。

設(shè)置 process.title 的成本很高

另一個(gè)跳出的函數(shù)調(diào)用是對(duì)神秘 title 屬性的 setter 的調(diào)用。設(shè)置屬性 20ms 似乎很昂貴。

該 setter 的實(shí)現(xiàn)非常簡(jiǎn)單:

class Npm extends EventEmitter {
  // ...
  set title(t) {
    // 這行代碼是罪魁禍?zhǔn)?
    process.title = t
    this.#title = t
  }
}

更改當(dāng)前正在運(yùn)行的進(jìn)程的標(biāo)題似乎是一個(gè)相當(dāng)昂貴的操作。不過(guò),此功能確實(shí)頗有用處,因?yàn)楫?dāng)您同時(shí)運(yùn)行多個(gè) npm 進(jìn)程時(shí),它可以更輕松地在任務(wù)管理器中發(fā)現(xiàn)特定的 npm 進(jìn)程。盡管如此,私以為可能值得深究是什么導(dǎo)致了如此昂貴的成本。

全局日志文件

配置文件中引起我注意的另一個(gè)入口是,對(duì) glob 模塊內(nèi)另一個(gè)字符串排序函數(shù)的調(diào)用。很奇怪的是,當(dāng)我們只想運(yùn)行 npm 腳本時(shí),我們甚至在這里進(jìn)行通配符。glob 模塊用于在文件系統(tǒng)中抓取與用戶定義模式匹配的文件,但為什么我們需要它呢?諷刺的是,大部分時(shí)間似乎不是花在搜索文件系統(tǒng)上,而是花在字符串排序上。

該函數(shù)僅使用包含 11 個(gè)字符串的簡(jiǎn)單數(shù)組調(diào)用一次,并且排序應(yīng)該是即時(shí)的。奇怪的是,配置文件顯示這花了大約 10 毫秒。

// 以某種方式排序此數(shù)組需要 10ms
;[
  '/Users/marvinhagemeister/.npm/_logs/2023-03-18T20_06_53_324Z-debug-0.log',
  '/Users/marvinhagemeister/.npm/_logs/2023-03-18T20_07_35_219Z-debug-0.log',
  '/Users/marvinhagemeister/.npm/_logs/2023-03-18T20_07_36_674Z-debug-0.log',
  '/Users/marvinhagemeister/.npm/_logs/2023-03-18T20_08_11_985Z-debug-0.log',
  '/Users/marvinhagemeister/.npm/_logs/2023-03-18T20_09_23_766Z-debug-0.log',
  '/Users/marvinhagemeister/.npm/_logs/2023-03-18T20_11_30_959Z-debug-0.log',
  '/Users/marvinhagemeister/.npm/_logs/2023-03-18T20_11_42_726Z-debug-0.log',
  '/Users/marvinhagemeister/.npm/_logs/2023-03-18T20_12_53_575Z-debug-0.log',
  '/Users/marvinhagemeister/.npm/_logs/2023-03-18T20_17_08_421Z-debug-0.log',
  '/Users/marvinhagemeister/.npm/_logs/2023-03-18T20_21_52_813Z-debug-0.log',
  '/Users/marvinhagemeister/.npm/_logs/2023-03-18T20_24_02_611Z-debug-0.log'
]

該實(shí)現(xiàn)看起來(lái)也人畜無(wú)害。

function alphasort(a, b) {
  return a.localeCompare(b, 'en')
}

但也許我們可以使用 Intl.Collator 對(duì)象來(lái)代替之前用來(lái)比較這些字符串的對(duì)象。

const collator = Intl.Collator('en')
function alphasort(a, b) {
  return collator.compare(a, b)
}

這就碼到功成了。我不完全確定為什么 String.prototype.localeCompare 相比之下更慢。這聽(tīng)起來(lái)確實(shí)很可疑。但我可以可靠地驗(yàn)證我這邊的速度差異。對(duì)于此特定調(diào)用,Intl.Collator 方法始終更快。

更大的問(wèn)題是,在文件系統(tǒng)中搜索日志文件似乎與我們的意圖不符。如果命令成功,日志文件會(huì)被寫入并清除,這非常有用,但是如果我們是最初創(chuàng)建這些文件的人,我們難道不應(yīng)該知道我們寫入的文件的名稱嗎?

此時(shí),我們已從最初的約 400 毫秒降至約 138 毫秒。盡管這已經(jīng)是一個(gè)相當(dāng)不錯(cuò)的優(yōu)化,但我們還可以更進(jìn)一步。

刪除所有東西

私以為我需要更加積極地刪除或取消注釋與運(yùn)行 npm 腳本無(wú)關(guān)的代碼。目前為止,我們已經(jīng)盡職盡責(zé),我們可以漸進(jìn)增強(qiáng),但我很好奇我們應(yīng)該爭(zhēng)取的預(yù)期時(shí)間是多少?;灸繕?biāo)是按需加載執(zhí)行 npm 腳本的代碼。其他一切都只是開(kāi)銷和時(shí)間浪費(fèi)。

所以我寫了一個(gè)簡(jiǎn)短的腳本,它只執(zhí)行運(yùn)行 npm 腳本所需的最低限度的工作。最后我把它降低到了大約 22 毫秒,這比我們開(kāi)始時(shí)的 400 毫秒快了大約 18 倍。我對(duì)此非常滿意,盡管與它的實(shí)際效果相比,22 毫秒仍然感覺(jué)很長(zhǎng)。相比之下,Rust 等其他語(yǔ)言無(wú)疑更擅長(zhǎng)這一點(diǎn)。無(wú)論如何,有一點(diǎn)需要指出的是,22 毫秒目前已經(jīng)足夠快了。

完結(jié)撒花

表面上看,我們花了那么多時(shí)間使 npm run 命令快了大約 380 毫秒,這似乎事倍功半。雖然但是,如果您考慮一下整顆地球的開(kāi)發(fā)者執(zhí)行該命令的頻率,以及在 CI 內(nèi)執(zhí)行該命令的頻率,這些優(yōu)化滾雪球驚人。對(duì)于本地開(kāi)發(fā)而言,擁有更快速的 npm 腳本也很棒,所以肯定存在個(gè)人利益的角度。

但房間里的大大象仍然存在:沒(méi)有簡(jiǎn)單的方法來(lái)短路模塊圖。目前為止,我見(jiàn)過(guò)的所有 JS 工具都存在此痛點(diǎn)。有些工具的影響更為明顯,而另一些工具則影響較小。解析和加載一堆模塊的開(kāi)銷非常真實(shí)。我不確定這個(gè)問(wèn)題的長(zhǎng)期解決方案是什么,或者 JS 引擎本身是否可以解決此問(wèn)題。

在找到合適的解決方案之前,我們今天可以應(yīng)用的一個(gè)可行的解決方案是,在將代碼發(fā)布到 npm 時(shí)將其打包。我私下希望這不是唯一可行的不二法門,并且所有運(yùn)行時(shí)都在這方面得到優(yōu)化。我們需要處理的工具越少,我們作為一個(gè)生態(tài)系統(tǒng)對(duì)初學(xué)者就越友好。

免責(zé)聲明

本文屬于是語(yǔ)冰的直男翻譯了屬于是,略有刪改,僅供粉絲參考

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

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

相關(guān)文章

最新評(píng)論