Vite,下一代 Web 工具

本文爲 Vue Conf 2021 分享內容。

分享者:李奎,Vue Core Team 成員,目前就職於 字節跳動 Web Infra 團隊

  1. 背景 =====

在 ESM 出現之前,由於瀏覽器缺少 JS 模塊化的機制以及頁面加載性能的問題,開發者都會打包來構建 Web App。期間 Webpack 等打包工具迅速流行在社區,被廣泛使用在項目中。但是,隨着項目的維護項目內部的 JS 模塊越來越多,這些打包工具在開發時遇到了性能瓶頸。相信大家或多或少的都有體驗,在 Webpack 的大型項目中需要等很長時間才能啓動 Dev Server,更新文件之後也需要經過一些時間頁面才能展示出最新的更改,這非常降低了開發者的開發效率以及體驗。Vite 正是爲了提高開發者的開發體驗而開發的工具,擁有極速的服務啓動和輕量快速的熱更新,2.0 發佈以來越來越多的用戶開始使用 Vite[1]。本文會對 Vite 進行剖析,讓大家對於 Vite 更加了解,在開發使用時更加得心應手。

  1. 基於 ESM 的 Dev Server + HMR ============================

首先,我們對比一下 vite 與 webpack 的啓動一個 vue hello world 時間。

0i4AXw

從上面的例子中可以看到 Webpack 啓動服務的時間較長一些,但是頁面加載性能是好於 vite 的。

2.1 Webpack(Bundle-Based Dev Server) 如何工作的呢?

從圖中可以看到 Webpack dev server 的啓動方式:

  1. 從 entry 開始分析依賴,bundle 依賴 (性能瓶頸),同時將入口文件注入到 index.html 中

  2. 啓動 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 的啓動方式:

  1. 不經過 Bundle,直接啓動 Dev Server

  2. 等待瀏覽器訪問文件,當請求文件時進行對文件那邊進行轉換返回給瀏覽器 (性能瓶頸)

2.3 Vite dev server 避免了 Bundle 的性能問題,但是也有一些新的問題:

2.4 爲了解決 Node 模塊的問題,Vite 引入 Pre-Bundle Node 模塊方案:

在項目啓動前,掃描項目內的所使用的 node 模塊,將 node 模塊打包成單個文件,這個操作是耗時的,但是由於 Node 模塊有自己的版本,可以將其寫入硬盤,下次啓動時如果版本匹配可以跳過 Pre-Bundle 使用硬盤緩存的結果。另外,Pre-Bunde 會生成模塊的元信息,通過識別引入的模塊並對其進行轉換,支持了 CJS 模塊的 Named Import。如下圖

使用工具:

那應該怎麼識別 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 怎麼工作的呢?

  1. 構建模塊依賴圖 當一個文件請求時,Vite 會掃描其中的 import 語法,記錄模塊之間的依賴關係

  2. 同時如果發現文件引用了 import.meta.hot 時會注入 helper 函數,並且模塊中含有 import.meta.hot.accept 的調用則將模塊標記成 boundary

  1. 當文件變更時,依據模塊依賴圖尋找 boundaries

  1. 發送 websocket 消息到瀏覽器端,瀏覽器會重新加載變更模塊並執行更新

  1. 如果沒有查找到 boundaries, 頁面則會重新加載

支持了 ESM Dev Server,但是並不能直接用於生產環境,爲了在生產環境獲得更好的加載性能,還需要 生產構建,對代碼進行體積優化(tree-shaking,minify)、chunk 合併分割等等。

  1. 基於 Rollup 的 Bundle 和 Plugins ===============================

由於已有的 Bundler 很成熟而且有良好的生態, vite 選擇在他們的基礎上進行用戶代碼的 Bundle。那如何選擇呢?Rollup 同樣基於 ESM ,而且其靈活的 Plugin API 以及體積更小、運行速度更快的構建產物顯然更爲合適。但是由於其對於 Web App 的支持度較低,而且配置複雜,非常不利於用戶的使用。爲此,Vite 內置了開發 Web App 常用的 Plugins,儘可能讓用戶可以零配置的使用。

對於 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 進行優化呢?

  1. 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。以下是一些實現 / 使用細節:

參考資料

[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