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

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

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

引言

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

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

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

npm 腳本

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

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

僅按需加載加載代碼

一大坨開發(fā)者不知道的是,npm CLI 是一個標準 JS 文件,可以像其他 .js 文件一樣執(zhí)行。在 macOS 和 Linux 上,您可以通過運行 which npm 獲取 npm cli 的完整路徑。將該文件轉(zhuǎn)儲到終端表明,它是一個平平無奇的標準 .js 文件。唯一奇葩在于首行代碼,它告訴 shell 可以使用哪個程序來執(zhí)行當前文件。因為我們正在處理一個 node 的 JS 文件。

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

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

npm 中存在這種情況,exit 句柄無腦 require。讓我們當且僅當需要時,才 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')
      //...
    }
  };

將更改后與未更改的配置文件比較,不會顯示總時間存在差異。這是因為我們在這里更改為延遲加載的模塊在其他地方餓漢式 require。為了正確地延遲加載它們,我們需要更改所有 require 的地方。

接下來我注意到,加載了一堆與 npm 審計功能相關(guān)的代碼。這看起來很奇葩,因為我沒有運行任何審計相關(guān)的東東。不幸的是,對我們而言,這并不像移動某些 require 調(diào)用那么容易。

萬能類

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

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

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

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 類稍后在其上擴展)都不需要。我們可以一鍵清空回收站。這一更改優(yōu)化了大約 20 毫秒,這可能看似九牛一毛,但積少成多。和以前一樣,我們需要檢查 require 這些模塊的其他地方,確保我們確實只按需加載它。

減小模塊圖大小

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

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

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

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

排序所有字符串

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

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

// @isaacs/string-locale-compare 中代碼的簡化示例
const collatorCompare = (locale, opts) => {
  const collator = new Intl.Collator(locale, opts)
  // 始終返回一個需要從零開始優(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
}

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

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

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

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

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

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

通過此優(yōu)化,調(diào)用該函數(shù)的時間趨近 0 毫秒。這又優(yōu)化了 10 毫秒,因為此乃最后一個餓漢式加載該模塊的地方。

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

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

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

該 setter 的實現(xiàn)非常簡單:

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

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

全局日志文件

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

該函數(shù)僅使用包含 11 個字符串的簡單數(shù)組調(diào)用一次,并且排序應(yīng)該是即時的。奇怪的是,配置文件顯示這花了大約 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'
]

該實現(xiàn)看起來也人畜無害。

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

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

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

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

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

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

刪除所有東西

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

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

完結(jié)撒花

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

但房間里的大大象仍然存在:沒有簡單的方法來短路模塊圖。目前為止,我見過的所有 JS 工具都存在此痛點。有些工具的影響更為明顯,而另一些工具則影響較小。解析和加載一堆模塊的開銷非常真實。我不確定這個問題的長期解決方案是什么,或者 JS 引擎本身是否可以解決此問題。

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

免責聲明

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

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

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

相關(guān)文章

最新評論