Vite,下一代 Web 工具
本文爲 Vue Conf 2021 分享內容。
分享者:李奎,Vue Core Team 成員,目前就職於 字節跳動 Web Infra 團隊
- 背景 =====
在 ESM 出現之前,由於瀏覽器缺少 JS 模塊化的機制以及頁面加載性能的問題,開發者都會打包來構建 Web App。期間 Webpack 等打包工具迅速流行在社區,被廣泛使用在項目中。但是,隨着項目的維護項目內部的 JS 模塊越來越多,這些打包工具在開發時遇到了性能瓶頸。相信大家或多或少的都有體驗,在 Webpack 的大型項目中需要等很長時間才能啓動 Dev Server,更新文件之後也需要經過一些時間頁面才能展示出最新的更改,這非常降低了開發者的開發效率以及體驗。Vite 正是爲了提高開發者的開發體驗而開發的工具,擁有極速的服務啓動和輕量快速的熱更新,2.0 發佈以來越來越多的用戶開始使用 Vite[1]。本文會對 Vite 進行剖析,讓大家對於 Vite 更加了解,在開發使用時更加得心應手。
- 基於 ESM 的 Dev Server + HMR ============================
首先,我們對比一下 vite 與 webpack 的啓動一個 vue hello world 時間。
從上面的例子中可以看到 Webpack 啓動服務的時間較長一些,但是頁面加載性能是好於 vite 的。
2.1 Webpack(Bundle-Based Dev Server) 如何工作的呢?
從圖中可以看到 Webpack dev server 的啓動方式:
-
從 entry 開始分析依賴,bundle 依賴 (性能瓶頸),同時將入口文件注入到 index.html 中
-
啓動 Webpack-dev-server,等待瀏覽器訪問 問題很明顯:
2.2 那 Vite dev server 是如何提高性能的呢?
ESM 是 ES6 引入的模塊化能力,現已經被主流瀏覽器支持,當 import 模塊時,瀏覽器就會下載被導入的模塊。
Vite 的 Dev server 基於瀏覽器原生支持 ESM 的能力實現的,因此不需要通過 Bundler 即可加載 JS 模塊,但是要求用戶的代碼必須是 ESM 模塊,而且需要在 index.html 中使用 <script type="module" src="./main.js"/>
來引入模塊
從圖中可以看到 Vite 的啓動方式:
-
不經過 Bundle,直接啓動 Dev Server
-
等待瀏覽器訪問文件,當請求文件時進行對文件那邊進行轉換返回給瀏覽器 (性能瓶頸)
2.3 Vite dev server 避免了 Bundle 的性能問題,但是也有一些新的問題:
-
文件 transform 性能
-
模塊轉換時儘可能使用性能高的工具
-
緩存 transform 結果
-
非 ESM 模塊兼容 (TS/JSX ...)
-
將非 ESM 模塊轉換成 ESM,依靠文件類型來辨別模塊類型
-
用 esbuild 轉換 TS/JSX,代替 TSC/Babel
-
Broswer ESM 不能加載 Node 模塊
-
使用 es-module-lexer 掃描 import 語法
-
magic-string 重寫 Node 模塊的引入路徑,如下圖
-
Node 模塊其他問題
-
Node CJS 模塊兼容
-
Node 模塊一般文件數量較多,如果直接加載,一個文件會產生一個請求,導致頁面加載性能降低
2.4 爲了解決 Node 模塊的問題,Vite 引入 Pre-Bundle Node 模塊方案:
在項目啓動前,掃描項目內的所使用的 node 模塊,將 node 模塊打包成單個文件,這個操作是耗時的,但是由於 Node 模塊有自己的版本,可以將其寫入硬盤,下次啓動時如果版本匹配可以跳過 Pre-Bundle 使用硬盤緩存的結果。另外,Pre-Bunde 會生成模塊的元信息,通過識別引入的模塊並對其進行轉換,支持了 CJS 模塊的 Named Import。如下圖
使用工具:
-
v1: Rollup + @rollup/plugin-commonjs @rollup/plugin-commonjs 的方案是將 cjs 代碼直接轉爲 esm,cjs 模塊的導入導出方式過於動態 而且 cjs 循環引用問題(19.0.0 版本修復)導致打包成 ESM 失敗
-
v2: esbuild 其支持 cjs 的方案是生成 helper 函數,兼容性好
那應該怎麼識別 Node 模塊進行 Pre-Bundle 呢,vite 支持了用戶自己配置和自動依賴掃描的功能。自動依賴掃描是掃描用戶全部代碼並識別其中引入的 node 模塊,由於 esbuild 的打包性能是 rollup 的 10-100 倍,性能也沒有造成下降。
2.5 ESM HMR
Vite ESM HMR API 借鑑於 Webpack HMR API,當某個模塊發生變化時,不用刷新頁面就可以更新對應的模塊。
首先看個 Vite HMR 使用的例子
import foo from './foo.js'
foo()
if (import.meta.hot) {
import.meta.hot.accept('./foo.js' ,(newFoo) => {
newFoo.foo()
})
}
轉換後的結果
import { createHotContext as __vite__createHotContext } from "/@vite/client";
import.meta.hot = __vite__createHotContext("/hmrDep.js");
import foo form '/foo.js'
foo()
if (import.meta.hot) {
import.meta.hot.accept("/foo.js" ,(newFoo) => {
newFoo.foo()
})
}
首先介紹一個概念 boundary 代表接受更新的模塊,如例子中引入 ./foo.js 的模塊
import.meta[2] 是一個給 JavaScript 模塊暴露特定上下文的元數據屬性的對象元信息的方式。
可以看到通過注入 helper 函數,給模塊引入了 import.meta.hot API,這個 API 會在瀏覽器運行時記錄 boundary 與模塊直接的映射關係(包含更新執行的回調函數),當 ws 接收到某個模塊更新信息(boundary 和 發生更新的模塊)時,會發起對更新模塊的加載,並且會根據模塊的更新信息從映射關係中查找到更新需要執行的回調函數,執行並傳入更新後的模塊。這樣就可以無需刷新頁面就可以更新 JS 模塊了。
那 vite 的 HMR 怎麼工作的呢?
-
構建模塊依賴圖 當一個文件請求時,Vite 會掃描其中的 import 語法,記錄模塊之間的依賴關係
-
同時如果發現文件引用了 import.meta.hot 時會注入 helper 函數,並且模塊中含有 import.meta.hot.accept 的調用則將模塊標記成 boundary
- 當文件變更時,依據模塊依賴圖尋找 boundaries
- 發送 websocket 消息到瀏覽器端,瀏覽器會重新加載變更模塊並執行更新
- 如果沒有查找到 boundaries, 頁面則會重新加載
支持了 ESM Dev Server,但是並不能直接用於生產環境,爲了在生產環境獲得更好的加載性能,還需要 生產構建,對代碼進行體積優化(tree-shaking,minify)、chunk 合併分割等等。
- 基於 Rollup 的 Bundle 和 Plugins ===============================
由於已有的 Bundler 很成熟而且有良好的生態, vite 選擇在他們的基礎上進行用戶代碼的 Bundle。那如何選擇呢?Rollup 同樣基於 ESM ,而且其靈活的 Plugin API 以及體積更小、運行速度更快的構建產物顯然更爲合適。但是由於其對於 Web App 的支持度較低,而且配置複雜,非常不利於用戶的使用。爲此,Vite 內置了開發 Web App 常用的 Plugins,儘可能讓用戶可以零配置的使用。
-
TS/JSX
-
PostCSS / CSS Modules / CSS Pre-processors
-
Assets
-
JSON
-
Web Worker
-
Module Resolver / Module Alias / Module Glob Import
-
...
對於 Framwork 的支持,Vite 官方集成了 Vue3(@vitejs/plugin-vue[3]) 、React (@vitejs/plugin-react-refresh[4]),而且提供了開箱即用的模版供用戶選擇。
另外,Vite Plugins 繼承了 Rollup Plugins API,並進行了一些拓展(ssr、hmr 支持等),這樣用戶可以利用社區內已有的 Rollup Plugins,或者開發 Plugins 滿足自己不同場景的需求。由於 Dev 是以文件爲單位單獨 transfrom,Bundle 是以項目爲單位構建,造成了 dev 和 Bundle 有一定的差異。爲了減少這種差異性,Vite 受 WMR 啓發,通過 PluginContainer 模擬了 Bundle 時 Plugns 的行爲,支持了在 Dev 環境中運行 Plugins。
對跑在瀏覽器內的 Web App 進行了體驗優化,那能不能更進一步對跑在 Node 中的 Web App 進行優化呢?
- SSR ======
SSR 是支持在 Node.js 中運行相同應用程序的前端框架(例如 React、Vue 等),將其預渲染成 HTML 返回客戶端,用於解決 SPA 的 SEO 和 首屏性能問題。
和開發 Web App 一樣,SSR 的開發環境也需要 Bundle,同樣也有相同的性能問題。那如何解決呢?ESM Based Dev Server 依靠了瀏覽器原生支持了 ESM,那 Node.js 是否可以支持原生加載 ESM 呢?答案是肯定的,Node.js 在 12.22.0 版本支持了標準的 ESM 實現,提供了 Loaders API[5],但是仍是實驗性質的而且需要 --experimental-loader 開啓。爲此,Vite 通過轉換 ESM 模塊導入導出相關的語法,並提供相應了 API,實現了自己的 Node ESM 模塊加載器,而且複用了 Dev Server PluginContainer 支持了 Plugins + HMR。以下是一些實現 / 使用細節:
-
由於 Vite 實現 Node ESM 模塊加載器 ,用戶需要通過 ssrLoadModule API 來導入入口模塊,此方法會遞歸加載子模塊並執行。
-
使用 estree + magic-string 轉換 ESM 模塊,如下圖 import 語法會轉換成 vite_ssr_import API。
- 另外,由於運行在 Node.js 環境中,Node CJS 模塊的加載可以不經過轉換,直接 require 執行。
參考資料
[1] Vite: https://www.npmtrends.com/vite
[2] import.meta: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/import.meta
[3] @vitejs/plugin-vue: https://github.com/vitejs/vite/tree/main/packages/plugin-vue
[4] @vitejs/plugin-react-refresh: https://github.com/vitejs/vite/tree/main/packages/plugin-react-refresh
[5] Loaders API: https://nodejs.org/api/esm.html#esm_loaders
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/M2fmiaVOj--h9bK3gT1X8A