avatar
threadsinstagram

Vite 系列總整理

Table of Contents

前言

這整個系列文轉眼間也要進入下半場,在 Day 9 到現在的 Day 18 不小心用了有一些多的篇幅在研究 Vite,其實原本還有 HMR、Module Federation 想研究,但盤點完大綱後覺得畢竟主題中還有 Rust 這回事,還是要拉回來一些。於是想在這裡先打住做個 Vite 系列總整理,後面篇幅還夠的話再來補上。

今天這篇中會根據 Day 9 開頭列的幾個問題一一做個簡短的重點整理,另外也會把前幾天追原始碼的脈絡說明一下。

關於 Vite 的問題

Q1. 為什麼 Vite 的冷啟動可以這麼快?

可以記住這兩張圖的比較:

bundle-based

Vite 利用瀏覽器原生支援 ESM 載入的特性,在冷啟動時直接按需載入需要的模組,因此不需要像以往 bundle-based 的 dev server 一定要走過整個分析依賴與打包的流程。

Q2. Vite 的 pre-bundling 是什麼?

因為在瀏覽器想要以 ESM 的方式去載入第三方套件時,可能會遇到幾個問題:

  • 套件輸出的模組化標準為 CJS、UMD 等
  • 大型套件內有依賴多個小模組,直接載入會有 request waterfall
  • 實際在瀏覽器上載入時的相對路徑複寫

為了解決以上三個問題,Vite 在啟動 dev server 前會用 esbuild 快速地將專案中有使用到的第三方套件預先打包到 node_modules/.vite/deps 底下,這就是 pre-bundling。

Q3. 如果專案很大的話,esbuild 在做 pre-bundle 真的不會有效能瓶頸嗎?

Day 15 的實驗中可以得到一個結論,當今天專案中載入了許多複雜的大型依賴套件,第一次在做 pre-bundle 時仍會稍微花一些時間打包,但時間上仍比大型的 Webpack 專案快上許多。

在有執行過第一次的 pre-bundle 後,打包後的內容會被放在 node_modules/.vite/deps 做快取,因此之後在啟 dev server 時都能在短時間內準備好。

Q4. Vite 跟 esbuild 的關係是什麼?

Vite 是 dev server,esbuild 是 bundler。Vite 在啟動 dev server 時會做 pre-bundling,而這個任務就是利用 esbuild 來幫忙快速打包出第三方套件們的 ESM 模組。

Q5. 為什麼 Vite 要用 Rollup 做為 production build 的 bundler,而不也繼續用 esbuild?

這問題也可以換個問法:「為什麼 Vite 要用兩套 bundler?」

Day 17 中有講到,esbuild 目前做為 bundler 雖然快但有許多限制:

  • esbuild 的 plugin 生態系不如 WebpackRollup 成熟
  • 在 build assets 的優化做的不夠多,像是目前在 code splitting 上仍有待解決的問題

而在 production build 方面,Rollup 能補足上述的不足,因此最終採用它來做這部份的工作。但 Rollup 也有一些待解決的問題:

  • 儘管 Rollup v4 開始將 parser 的部份從 Babel 換成 Rust-based 的 SWC,但整體打包速度上比起 Rust-based bundler 仍太慢
  • 在處理 ESM/CJS 的轉換上有時會遇到一些 tricky issue

對這段說明有興趣也可以參考 Evan You 在 ViteConf 2023 這一段的說明:

Q6. 什麼是 Rolldown?跟 Rollup 差在哪?

上面提到 esbuild 與 Rollup 做為 bundler 有各自的限制與問題。且因為現在 dev 與 production 使用兩套不同的 bundler,有時候開發者會遇到一些只有在 production 才有的問題,這時就會不好做 debug。

因此 Vite 在 2023 年底開始與 Rspack 團隊合作從頭打造一款 Rust-based bunder —— Rolldown,想在 Vite v6 中統一兩種環境的 bundler,且利用 Rust 編譯語言的效能來提升 production build 的速度。

Q7. 什麼是 OXC?跟 SWC 有何關係?為什麼 Vite 底層會需要這個工具?

vueconf 2024

這個後面如果篇幅夠可以再來細講跟做實驗玩玩看,但簡單說明的話 OXC 是一套用 Rust 開發的 JS 工具鏈,裡面分別有實作 parser、linter、transformer 等功能。

而前面提到原本在 Rollup 中用來做為 JS parser 的 Babel 在 v4 時有替換成更快的 SWC (Speedy Web Compiler),而這個 OXC parser 的效能評測上看起來又比 SWC 快上 3 倍,看起來 Vite 團隊打算在打造 Rolldown 的底層時直接用上,可預見一個快上加快的 bundler 正在醞釀中。

Q8. HMR (Hot Module Replacement) 的原理是什麼?

HMR (熱模組更新) 簡單說明的話就是指在開發過程中,在檔案變更後,dev server 可以在不重整頁面的情況下,就直接幫你局部替換掉畫面上為新的模組。

最常拿來比較的對象是 Live Reload,兩者的差異在於 Live Reload 會在檔案變更後去重整頁面,但這樣的缺點是可能其他沒被更動的模組的狀態也會因為頁面被刷新而被重置,在開發上較不方便。

至於 HMR 的原理的話前面在 Day 13 中有追到一個叫做 Chokidar 的套件會幫忙做檔案異動的監聽,在接到檔案異動的事件後會去觸發的這個 onHMRUpdate 應該就是原理所在,一樣也是後面篇幅還夠時再來繼續研究。

試著追 Vite 原始碼重點整理

前面也花了些篇幅來嘗試追看看開源專案原始碼,一路從啟 dev server 到執行 pre-bundling 完成可以整理如下:

  • 啟 dev server
    • cli.alias('dev')
    • createServer
      • connect 套件建立 middlewares
      • 建立 WebSocket server
      • Chokidar 套件來做為 watcher 監聽檔案變化 (HMR 主要邏輯所在)
  • pre-bundling 的進入點
    • createServer
    • initServer
    • environment.depsOptimizer?.init()
    • 其中的 depsOptimizer 中有兩種環境與設定條件:
      • 一個是 createExplicitDepsOptimizer
      • 一個是 createDepsOptimizer
  • pre-bundling 前置作業
    • createDepsOptimizer
      • 用來做 optimizer 的初始化
    • loadCachedDepOptimizationMetadata
      • 算 metadata 與 hash
    • discoverProjectDependencies
      • scanImports
      • 專案中用了哪些第三方套件,有的話會補上 flattenId
    • runOptimizeDeps
    • prepareEsbuildOptimizerRun
      • 準備啟動 esbuild 的相關設定
  • 執行 pre-bundling
    • esbuild.context.rebuild
  • 最後 pre-bundle 後將結果放在 node_modules/.vite/deps 底下

也把上述的探索路徑做成圖示版:

vite source map

而關於 Vite 原始碼這只算是冰山一角,其他也還有 HMR、Module Graph、plugin、npm run build 等沒追,對這部份有興趣的讀者歡迎參考以上追過的筆記,也強烈推薦搭配菜市場阿龍的《Vite 原始碼解讀》影片版可能更好理解。

小結

今天將前面提到的 Vite 的幾個問題做了總整理,也把這幾天以來追的 Vite 原始碼做成了完整版的圖示,也算是一個 Vite 的精彩完結篇,如果有什麼問題或講錯的地方都在麻煩留言給我,感謝你的閱讀!

從明天開始就要正式進入「Rust 的戰國時代」的部份了,我們明天見!