iT邦幫忙

2024 iThome 鐵人賽

DAY 15
0
Modern Web

Rust 的戰國時代:探索網頁前端工具的前世今生系列 第 15

Day 15:來做個實驗 - 壓測 Vite 的效能瓶頸

  • 分享至 

  • xImage
  •  

day_15_banner

前言

看了幾天的 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%

模擬一個複雜的大型專案

如果要模擬一個複雜的有複雜依賴關係的專案,覺得可以朝幾個方向去做:

  • 寫一個 script 產出非常深層的依賴元件
  • 在最深層的這個元件中去載入多個第三方大型套件,並做一些複雜資料的處理

檔案結構說明

最後預期的檔案結構應該會像是這個樣子:

structure

在資料夾下的檔案結構會長成這樣,或可以參考今日實驗的原始碼位置

/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 這個資料夾裡

快速建出深層巢狀依賴的 Vue 元件們

首先先來寫一個 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),就可以快速產出層層依賴的模組們:

vue_files

在 LeafComponent 中引入多個複雜第三方套件

下一步試著在最深層的 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-plusprimevue,以及一些 table 及 chart 的套件等。

產大量假資料到 large-data.json 中

最後把建立大量假資料到 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 產出多筆假資料,另外會在 BigComponentbig-utils 中用一些測試用的資料處理邏輯與畫面處理,就不一一列出,有興趣深入改寫測試的人也可以參考範例程式碼中的這幾個檔案

手動調整 App.vue 的引入元件

有個小地方要注意是,上面沒有將建立模組數量的參數抽出來,一開始做實驗時不小心踩到 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 來測試看看效果了,執行後可以看到這樣的結果:

result

第一次冷啟動時看起來是有比空專案久一些,但後來有時候把整個 node_modules/.vite/deps 清掉時重跑,反而會是少於一秒,看起來效果跟預期中的不一樣,預期以為會每次重新跑 pre-bundling 都會需要花一些時間。在想也有可能是載入的第三方套件還不算太複雜或實驗手法哪邊會需要調整。

不過倒是有發現一個有趣的現象是,在實際連到這個網頁去載入時,因為剛做了巢狀載入一萬個檔案,也是會遇到 request waterfall 的問題,某種程度上好像算是有壓測到一些 Vite 使用上的限制:

request waterfall

在想有可能 Vite 應該有一些設定可以調整,可能之後再來研究看看。

小結

今天試著做一個實驗來壓力測試 pre-bundling 在大型專案中的效能瓶頸,雖然結果上跟預期的不完全符合,但過程中也學到許多知識點像是如何快速用 Node.js 產檔案與假資料、如何把測試變數抽成 .env 中在 Vue 中搭配動態載入方式使用,或許之後有想要再做效能壓測可以回頭用這個專案再來實驗看看。

或有興趣繼續做實驗的讀者也歡迎延伸這份程式碼試試看,有發現什麼有趣的結果或我寫錯的地方再留言告訴我,感謝你的閱讀!


上一篇
Day 14:來試著追一下 Vite 原始碼 (3) - pre-bundling 怎麼做套件分析
下一篇
Day 16:來試著追一下 Vite 原始碼 (4) - pre-bundling 最終章
系列文
Rust 的戰國時代:探索網頁前端工具的前世今生30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言