看了幾天的 pre-bundling 原始碼,在進到最後打包階段程式碼前,我想先實驗一下前幾天研究時突然好奇的一個問題:「如果一個拿 Vite 當 build tool 的專案越長越大,在冷啟動時真的不會有效能瓶頸嗎?」
剛好看了幾天的原始碼今天手有點癢,就來實際做一下實驗好了。
在前面這篇文章中理解了 pre-bundling 的原理後,我們知道為了要讓 dev server 可以用上 native ESM 做開發,在第一次冷啟動時,需要透過 pre-bundling 預先做打包解決 request 過多及模組轉譯的處理,等於 pre-bundling 這個步驟可能會是一個權衡下的成本。
那假如某個大型專案中,使用許多第三方大型依賴套件、模組依賴很複雜時,會不會最後還是會遇到第一次冷啟動緩慢的問題。
參考文件中在效能中有提到類似的問題,這裡我們就實際來試試看要怎麼重現。
首先先啟一個基本的 Vue 專案:
$ pnpm create vite vite-try-large-vue-ts --template vue-ts
$ cd vite-try-large-vue-ts
$ pnpm install
$ pnpm run dev
在使用 pnpm run dev
後會得到這樣的結果:
VITE v5.4.8 ready in 2411 ms
➜ Local: http://localhost:5173/
此時如果試著 ctrl + c 停掉 server 後再重啟一次 server 會是這樣的結果:
VITE v5.4.8 ready in 290 ms
➜ Local: http://localhost:5173/
看起來跟預期中的一樣,第一次因為沒有做 pre-bundling 所以會需要一些時間來利用 esbuild 打包,而第二次因為已經將打包過的快取資料放在 node_modules/.vite/deps
中,所以可以略過重打包,因此啟動時間減少了 87.97%
。
如果要模擬一個複雜的有複雜依賴關係的專案,覺得可以朝幾個方向去做:
最後預期的檔案結構應該會像是這個樣子:
在資料夾下的檔案結構會長成這樣,或可以參考今日實驗的原始碼位置:
/vite-try-large-vue-ts
├── genData.cjs // 用來快速產出巢狀依賴模組、假資料的 script
├── index.html
├── src
| ├── App.vue
| ├── components
| | ├── BigComponent.vue
| | ├── LeafComponent.vue
| | ├── big-utils.ts
| | ├── large-data.json
| | └── nested
| | ├── Module0.vue
| | ├── Module1.vue
| | ├── ...
| | ├── Module9998.vue
| | └── Module9999.vue
| ├── main.ts
└── vite.config.ts
genData.cjs
:可以快速產生巢狀依賴的 Vue component 到 src/components/nested
中,以及建立 large-data.json
的 Node.js script
src/components/nested
:巢狀依賴的 Vue 元件們,為了方便管理另外放在 nested
這個資料夾裡
首先先來寫一個 Node.js script 可以快速產生巢狀依賴的 Vue component,這裡直接在 root 新增一個檔案 genData.cjs
:
// genData.cjs
const fs = require('fs');
const path = require('path');
// 輸出目錄
const outputDir = path.resolve(__dirname, 'src/components/nested');
// 如果輸出目錄已經存在,則刪除後重新建立
if (fs.existsSync(outputDir)) {
fs.rmSync(outputDir, { recursive: true, force: true });
}
fs.mkdirSync(outputDir, { recursive: true });
// 生成巢狀依賴的 Vue 元件們
function generateVueModules(count) {
for (let i = 0; i < count; i++) {
const moduleId = i - 1;
let content;
// 在最後一個深層元件中去引入 LeafComponent
if (i === 0) {
content = `
<script setup lang="ts">
import LeafComponent from '../LeafComponent.vue';
</script>
<template>
<LeafComponent :count="count" />
</template>
`;
} else {
// 其他元件則依照編號順序載入彼此
content = `
<script setup lang="ts">
import { ref } from 'vue';
import Module${moduleId} from './Module${moduleId}.vue';
const props = defineProps({
count: {
type: Number,
required: true,
default: 0
}
});
const countData = ref(props.count + 1);
</script>
<template>
<Module${moduleId} :count="countData" />
</template>
`;
}
// 將上面的內容寫入文件中
fs.writeFileSync(path.join(outputDir, `Module${i}.vue`), content);
}
}
// 先產生少量測試一下是否能正確運行
generateVueModules(5);
將說明都放在上面註解上,基本上就是要做到這些事:
count
來跑 for 迴圈,產出多個彼此依賴的 Vue 元件Module0.vue
的內容會取載入 LeafComponent
,另外抽出來後續做其他複雜的處理有個開發時的測試小技巧是雖然最後我們想要指定產出數千、數萬個檔案,但一開始可以先用 generateVueModules(5)
產生少量來測試是否有能正確執行。
最後實際用 node ./genData.cjs
執行後,應可以正確產出模擬的檔案們,此時如果將參數調成 generateVueModules(10000)
,就可以快速產出層層依賴的模組們:
下一步試著在最深層的 LeafComponent
去嘗試模擬載入許多可能比較複雜的第三方套件的狀況,先嘗試安裝各種套件:
$ pnpm add lodash-es @vueuse/core element-plus ag-grid-vue3 moment gsap echarts quill primevue
在 LeafComponent
中去載入來做個大雜燴,因為只是實驗載入套件畫面部份先不細調,程式碼內容因為是測試內容就不全部列出,可以參考這個完整原始碼:
<script setup lang="ts">
import _ from 'lodash-es';
import moment from 'moment';
import { ref, onMounted } from 'vue';
import { useNow } from '@vueuse/core';
import { ElButton } from 'element-plus';
import { AgGridVue } from 'ag-grid-vue3';
import * as echarts from 'echarts';
import { gsap } from 'gsap';
import Quill from 'quill';
import Button from 'primevue/button';
import BigComponent from './BigComponent.vue';
import { someUtilFunction } from './big-utils';
import largeData from './large-data.json';
// ... 略 ...
</script>
這裡試著找一些比較大的第三方套件像是 UI library 的 element-plus
跟 primevue
,以及一些 table 及 chart 的套件等。
最後把建立大量假資料到 large-data.json
檔的邏輯也一起加到中 genData.cjs
中:
// 產出隨機資料
function generateLargeData(count) {
const data = [];
for (let i = 0; i < count; i++) {
const id = crypto.randomUUID();
data.push({
id: id,
name: `Item ${id}`,
value: crypto.randomUUID()
});
}
return data;
}
// 寫入檔案
function writeLargeDataToFile(data) {
const filePath = path.resolve(__dirname, 'src/components/large-data.json');
if (!fs.existsSync(filePath)) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
}
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
}
// 實際執行快速生成大量數據並寫入文件
const largeData = generateLargeData(9999);
writeLargeDataToFile(largeData);
加上後再用 node ./genData.cjs
執行,就可以看到 large-data.json
產出多筆假資料,另外會在 BigComponent
、big-utils
中用一些測試用的資料處理邏輯與畫面處理,就不一一列出,有興趣深入改寫測試的人也可以參考範例程式碼中的這幾個檔案。
有個小地方要注意是,上面沒有將建立模組數量的參數抽出來,一開始做實驗時不小心踩到 bug 在 App.vue
中只載了測試用的 Module0.vue
,反覆測試後還想說怎麼沒效果。
因此這邊為了方便調校參數也把要產出幾層的巢狀依賴 Vue 元件的這個值加入設定檔中:
# .env
VITE_VUE_MODULE_COUNT=10000
接下來想把 App.vue
的地方可以根據 env 設定搭配 Vue 的 Async Component 做到動態載入,載入的地方看起來可以用 Suspense 這個語法來做 loading 的狀態,雖然目前看起來還是實驗性語法:
// src/App.vue
<script setup lang="ts">
import { ref, defineAsyncComponent } from 'vue'
const moduleCount = import.meta.env.VITE_VUE_MODULE_COUNT;
const modules = import.meta.glob('./components/nested/Module*.vue')
const modulePath = `./components/nested/Module${moduleCount - 1}.vue`
const ModuleComponent = defineAsyncComponent(() => modules[modulePath]())
</script>
<template>
<Suspense>
<template #default>
<ModuleComponent :count="10" />
</template>
<template #fallback>
Loading...
</template>
</Suspense>
</template>
再調整建假資料的 script,這裡會需要用 dotenv 處理設定檔參數的載入,先安裝 dotenv 套件:
$ pnpm add dotenv
載入 env 參數:
// genData.cjs
require('dotenv').config();
const moduleCount = process.env.VITE_VUE_MODULE_COUNT;
做了以上的調整後,可以順手再調整一下 package.json
中的 script,方便每次在執行 pnpm run dev
時能直接建假資料並啟 server:
"scripts": {
"dev": "node genData.cjs && vite --force",
...
},
假資料與專案一切準備就緒後,就可以實際用 pnpm run dev
來測試看看效果了,執行後可以看到這樣的結果:
第一次冷啟動時看起來是有比空專案久一些,但後來有時候把整個 node_modules/.vite/deps
清掉時重跑,反而會是少於一秒,看起來效果跟預期中的不一樣,預期以為會每次重新跑 pre-bundling 都會需要花一些時間。在想也有可能是載入的第三方套件還不算太複雜或實驗手法哪邊會需要調整。
不過倒是有發現一個有趣的現象是,在實際連到這個網頁去載入時,因為剛做了巢狀載入一萬個檔案,也是會遇到 request waterfall 的問題,某種程度上好像算是有壓測到一些 Vite 使用上的限制:
在想有可能 Vite 應該有一些設定可以調整,可能之後再來研究看看。
今天試著做一個實驗來壓力測試 pre-bundling 在大型專案中的效能瓶頸,雖然結果上跟預期的不完全符合,但過程中也學到許多知識點像是如何快速用 Node.js 產檔案與假資料、如何把測試變數抽成 .env
中在 Vue 中搭配動態載入方式使用,或許之後有想要再做效能壓測可以回頭用這個專案再來實驗看看。
或有興趣繼續做實驗的讀者也歡迎延伸這份程式碼試試看,有發現什麼有趣的結果或我寫錯的地方再留言告訴我,感謝你的閱讀!