成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

加速 JS 生態系統:模塊解析

開發 前端
我們工具的緩慢并不是由 JS 這種語言造成的,而是因為根本沒有優化。JS 生態系統的碎片化也無濟于事,因為沒有一個用于模塊解析的標準包。相反,有很多個包,并且它們都共享不同的功能子集。

大家好,這里是大家的林語冰。

長話短說:無論您是在構建、測試或檢查 JS,模塊解析始終是這一切的核心。盡管模塊解析在前端工具鏈中占據核心地位,但我們并沒有花太多時間來優化它。通過本文討論的變更,工具的速度優化 30%。

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

  1. PostCSS,SVGO 等等
  2. 模塊解析
  3. 使用 eslint
  4. npm 腳本
  5. draft-js emoji 插件
  6. polyfill 暴走
  7. 桶裝文件崩潰
  8. Tailwind CSS

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

在本系列的第 1 篇博客中,我們發現了若干種加速 JS 工具庫的方法。雖然這些低層補丁使總構建時間大大加速,但我們的工具中是否還有某些基建可以優化。那些對常見 JS 任務(比如打包、測試和 linting)的總時間影響更大的東東。

因此,我從我們行業常用的各種工具中收集了大約十幾個 CPU 配置文件。經過一番檢查后,我發現每個配置文件中都存在重復模式,該模式對這些任務的總運行時間的影響高達 30%。它是我們基建中茲事體大的部分,值得深入探討。

這個關鍵部分稱為模塊解析。在我所有調試中,它花費的時間比解析源碼還要多。

捕獲堆棧跟蹤的成本

當我注意到這些跟蹤中最耗時的方面花費在 captureLargerStackTrace 負責將堆棧跟蹤附加到 Error 對象的內部 Node 函數時,好戲就開始了。這似乎有點不同尋常,因為這兩項任務都成功了,并且沒有顯示任何拋出錯誤的跡象。

圖片圖片

單擊分析數據中的一系列事件后,可以更清晰地了解正在發生的情況。幾乎所有錯誤的產生都來自調用 Node 原生的 fs.statSync() 函數,而該函數又在名為 isFile 的函數內調用。文檔提到 fs.statSync() 基本上相當于 POSIX 的 fstat 命令,通常用于檢查磁盤上是否存在路徑,是文件還是目錄。考慮到這一點,我們應該只在文件不存在、缺乏文件讀取權限或類似特殊用例中獲取錯誤。是時候瞄一下 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
  }
}

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

查看 Node 的 fs.statSync 文檔可以發現,它支持傳遞 throwIfNoEntry 選項,該選項可防止在不存在文件系統入口時報錯。相反,在那種情況下它將返回 undefined。

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

應用此選項允許我們避免 catch 區塊中的 if 語句,這反過來又使 try/catch 變得多余,并允許我們進一步簡化函數。

這一簡單更改將項目的 lint 時間縮短了 7%。更棒的是,測試也從相同的更改中受益。

文件系統很昂貴

隨著該函數的堆棧跟蹤開銷被消除,我覺得還有一大坨優化空間。您知道的,在幾分鐘內捕獲的跟蹤中根本不應該出現幾個錯誤。因此,我在該函數中注入了一個簡單的計數器,了解其調用頻率。顯而易見,它被調用了大約 15k 次,大約是項目中文件數量的 10 倍。這聽起來像是一個優化的機會。

模塊與否,這是一個問題

默認情況下,工具需要了解三種說明符:

  • 相對模塊導入:./foo、../bar/boof
  • 絕對模塊導入:/foo、/foo/bar/bob
  • 包導入 foo、@foo/bar

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

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

  • /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/

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

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

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

緩存所有的東西

接下來,我增強了日志記錄,查看調用該函數的唯一文件路徑有多少個,以及它是否始終返回相同的結果。只有大約 2.5k 個對 isFile 的調用具有唯一的文件路徑,并且傳遞的文件參數和返回值之間存在強大的 1:1 映射。它仍然比項目中的文件數量多,但比總共被調用的 15k 次要低得多。如果我們在其周圍添加一個緩存,避免訪問文件系統,那會如何?

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 時間又加快了 15%。不過,緩存的風險在于,它們可能會變得過時。它們通常必須在某個時間點失效。為了安全起見,我最終選擇了一種更保守的方法來檢查緩存的文件是否仍然存在。如果您認為工具經常在監視模式下運行,那這種情況并不罕見,在監視模式下,期望盡可能多地緩存,并且僅使更改的文件無效。

const cache = new Map()

function resolve(file) {
  const cached = cache.get(file)

  // 保守策略:檢查緩存文件是否存在硬盤上
  // 避免監測模式下穩定緩存時文件可能移動或重命名
  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}`)
}

老實說,我本來希望它首先會抵消添加緩存的好處,因為即使在緩存場景中我們也會訪問文件系統。但從數字來看,這只使總 linting 時間惡化了 0.05%。相比之下,這是一個非常小的影響,但額外的文件系統調用不是更重要嗎?

文件擴展名猜謎游戲

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

實際上,我們在這里處理的是歧義,工具必須能夠理解 ./foo 的解析結果。這樣,很可能會產生浪費的文件系統調用,因為無法提前知道將文件解析到哪里。工具實際上必須嘗試每種組合,直到找到匹配項。如果我們看看目前存在的可能擴展的總數,情況會變得更糟。工具通常有一系列潛在的擴展需要檢查。如果您包含 TS,那么在撰寫本文時,典型前端項目的完整列表為:

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

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

  1. foo.js -> 不存在
  2. foo.jsx -> 不存在
  3. foo.cjs -> 不存在
  4. foo.mjs -> 不存在
  5. foo.ts -> 終于找到了!

這是四個不必要的文件系統調用。當然,您可以更改擴展的順序,并將項目中最常見的擴展放在數組的開頭。這會增加提前找到正確擴展的機會,但并不能完全消除問題。

作為 ES2015 規范的一部分,提出了一個新的模塊系統。所有細節都沒有及時充實,但語法卻充實了。import 語句很快就占據了主導地位,因為它們在工具方面比 CommonJS 有優勢。由于其靜態性,它為更多工具增強功能開辟了空間,比如最著名的 tree-shaking(樹搖優化),其中未使用的模塊甚至模塊中的功能可以輕松檢測到,并從生產版本中刪除。自然而然地,每個人都接受了新的導入語法。

但有一個問題:只最終確定了語法,而不是實際的模塊加載或解析應該如何工作。為了填補這一空白,工具重新使用了 CommonJS 中的現有語義。這對于采用是有好處的,因為移植大多數代碼庫只需要進行語法更改,并且這些可以通過 codemods 自動化。從采用的角度來看,這是一個很棒的方面!但這也意味著,我們繼承了導入說明符應解析為哪個文件擴展名的猜測游戲。

模塊加載和解析的實際規范在幾年后最終確定,并通過強制擴展糾正了這個錯誤。

// 非法 ESM,導入說明符缺失擴展名
import { doSomething } from './foo'

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

通過消除這種歧義源并始終添加擴展,我們可以避免一整類問題。工具也變得更快。但生態系統在這方面取得進展(甚至根本沒有取得進展)還需要時間,因為工具已經適應了處理模糊性的問題。

路在何方?

在整個調查過程中,我有點驚訝地發現在優化模塊分辨率方面還有一大坨優化空間,因為它是我們工具的核心。本文中描述的若干更改將 linting 時間減少了 30%!

我們在這里所做的若干優化也不是 JS 獨有的。這些優化與其他編程語言的工具中可以找到的優化相同。當談到模塊分辨率時,四個主要要點是:

  • 盡可能避免調用文件系統
  • 盡可能緩存,避免調用文件系統
  • 當使用 fs.stat 或 fs.statSync 時,請始終設置 throwIfNoEntry: false
  • 盡可能限制向上遍歷

我們工具的緩慢并不是由 JS 這種語言造成的,而是因為根本沒有優化。JS 生態系統的碎片化也無濟于事,因為沒有一個用于模塊解析的標準包。相反,有很多個包,并且它們都共享不同的功能子集。這并不奇怪,因為多年來支持的功能列表不斷增長,并且在撰寫本文時還沒有一個庫可以支持所有這些功能。擁有一個每個人都使用的單一庫能使每個人一勞永逸地解決此問題。

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

[1]Speeding up the JavaScript ecosystem - module resolution: https://marvinh.dev/blog/speeding-up-javascript-ecosystem-part-2

責任編輯:武曉燕 來源: 人貓神話
相關推薦

2015-02-11 17:40:14

APICloud

2011-12-09 11:02:52

NoSQL

2023-08-03 10:17:57

JavaScripNode.jsChrome

2010-05-12 11:16:00

SAP

2013-11-04 16:57:21

Hadoop大數據Hadoop生態系統

2011-05-19 15:15:39

Oracle生態系統

2023-03-10 15:18:38

2015-04-01 11:23:23

2015-06-08 12:44:58

大數據InterlAMPCamp

2024-03-07 09:00:00

微服務架構共享平臺開發

2019-01-13 15:00:52

區塊鏈生態系統

2021-11-23 20:54:34

AI 生態系統

2017-08-02 13:08:30

物聯網生態系統邊緣計算

2009-12-25 14:49:55

2022-02-25 11:09:16

區塊鏈技術生態系統

2023-10-11 15:11:08

智能建筑人工智能

2014-12-25 19:01:34

PTC物聯網

2011-04-26 10:08:47

Linux存儲生態環境

2017-10-13 15:41:22

軟件開發圖譜

2025-05-26 01:00:00

AI人工智能云原生
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 精品国产成人 | 蜜桃视频在线观看免费视频网站www | 国产精品久久久久久久久久三级 | 亚洲精品一区二区三区丝袜 | 亚洲欧美综合 | 色婷婷久久久亚洲一区二区三区 | 一级欧美一级日韩片免费观看 | www日本高清视频 | 人操人人干人 | 国产日韩欧美精品一区二区 | 国产亚洲精品成人av久久ww | 国产91一区二区三区 | 欧美成人久久 | 久久成人精品一区二区三区 | 亚洲97| 国产精品免费看 | 蜜臀网 | 欧美亚洲视频在线观看 | 韩日一区二区 | 欧美激情精品久久久久久变态 | 亚洲免费视频在线观看 | 91视频中文 | 欧美成人不卡 | 嫩草网 | 在线观看国产h | 亚洲一区二区三区桃乃木香奈 | 成人天堂 | 嫩草最新网址 | 在线国产欧美 | 视频一区中文字幕 | 精品伊人久久 | 亚洲国产在 | 国产日韩一区二区三免费高清 | 中文字幕免费视频 | 成人精品一区二区三区四区 | 欧美一区二区三区大片 | 在线免费激情视频 | 一区免费| 国产xxxx在线 | 精品在线一区二区三区 | 欧美精品中文字幕久久二区 |