大量 UI Rendering 時,Vue Runtime 的成本在哪裡?新版 Runtime 是否真的改善?
商品列表、Dashboard、CRM、管理後台,都可能同時渲染數百甚至數千個 Card、Row 或 Cell,難免 一次就要處理大量 UI。
實務上最常見 UI 從 100 個增加到 5,000 個時網頁載入速度變慢,成本究竟增加在哪裡?
因此 Day 17 建立第三個 Validation Scenario:VDOM Stress Test 。
如果直接拿真實專案測試,通常會同時包含:
最後即使得到一個 120ms,也很難判斷這個數字代表什麼,所以這次反過來處理,刻意建立一個足夠簡單、可以控制變因的壓力測試來取代一個完整的真實專案。
VDOM Stress Test
│
┌────────┴────────┐
│ │
Generate Data Render UI
│ │
└────────┬────────┘
▼
100 / 500 / 1,000 / 5,000
Scenario 只保留 資料產生 → Vue Render,刻意不加入 API、Pinia、Router、Watch、Computed Chain 或額外最佳化,這樣當 Render Count 改變時,主要改變的是 一次需要處理多少 UI。
畫面只需要改變 Render Count,再觸發一次 Render。

核心程式維持最小化:
const cards = ref<Card[]>([])
const renderDuration = ref(0)
async function triggerRender(count: number) {
const start = performance.now()
cards.value = generateCards(count)
await nextTick()
const end = performance.now()
renderDuration.value = end - start
}
這裡的 performance.now() 是第一層量測,主要觀察 這次操作從開始,到 Vue 完成這次更新 flush,花了多久?
例如:
Render Count = 1,000
renderDuration
│
▼
120 ms
這個數字很有用,但還不夠。
renderDuration = 120ms,只能表示這次操作觀察到約 120ms 的時間,這段時間裡可能包含不同工作:
120 ms
│
├── Application JavaScript
│
├── Vue Update / Patch
│
└── 其他同步工作
而且:
await nextTick()
代表 Vue 的更新 flush 已經完成,不代表瀏覽器已經完成 Layout、Paint,也不代表這個畫面已經完成最後的視覺呈現。
所以 renderDuration 能回答的是 「花多久」,但是還不能回答 「花在哪裡?」,這個差異會直接影響後面的 Vue 3.5 / 3.6 比較。
大量 UI Rendering 還有另一個容易被忽略的問題:第一次建立 5,000 個 UI,和已經存在 5,000 個 UI 後再次更新,不是同一件事。
第一次需要建立大量 UI:
Mount
100
↓
500
↓
1,000
↓
5,000
而 Update 則是:
Update
5,000 個 UI 已經存在
│
▼
再次觸發 Render
因此這個 Scenario 不把兩種成本混成一個數字,而是分開觀察:
| 測試 | 主要問題 |
|---|---|
| Mount | 大量 UI 第一次建立時,需要多少成本? |
| Update | 大量 UI 已存在,再次更新時需要多少成本? |
否則最後得到 5,000 UI = 300ms ,我們還是不知道這 300ms 到底主要發生在第一次建立,還是後續更新。
假設測到:
N = 1,000
Render Duration
└── 120 ms
這個結果只能說 這次操作很花時間,但這次實驗真正想回答的是:
120 ms
│
├── JavaScript?
│
├── Vue Update / Patch?
│
├── Recalculate Style?
│
├── Layout?
│
└── Paint?
這也是 VDOM Stress Test 和前面 Scenario 的重要差異,前面的實驗比較偏向 某種工作是不是變多、變慢?
這一次則進一步追問:
大量 UI 產生的成本,究竟落在哪一層?
一次 UI Rendering,可以先用下面這個方式理解:
Render
│
┌─────────┴─────────┐
▼ ▼
Vue / JavaScript Browser Rendering
│ │
│ ┌─────┼─────┐
│ ▼ ▼ ▼
│ Style Layout Paint
│
└── Update / Patch
這裡有一個很重要的觀念:
Chrome Performance 裡的 Rendering,不是 Vue Runtime。
Rendering 主要描述瀏覽器自己的 Rendering 工作。
同樣地:
renderDuration 也不是純粹的 Vue Runtime 時間。
因此這次實驗會使用不同層級的資料回答不同問題:
| 量測 | 回答的問題 |
|---|---|
renderDuration |
這次操作總共花多久? |
| JavaScript / Scripting | JavaScript 執行花多少? |
| Vue Runtime | Vue 本身的更新工作佔多少? |
| Rendering | Browser Rendering 花多少? |
| Layout / Paint | 瀏覽器後續工作花多少? |
這樣後面比較 Vue 3.5 與 Vue 3.6 時,才不會看到一個數字下降,就直接下結論 Vue 3.6 變快了,我們真正要確認的是 下降的是 Vue Runtime Cost,還是 Browser Rendering Cost?
到這裡,performance.now() 的角色就很清楚了。
performance.now()
│
▼
知道「花多久」
│
│
▼
Chrome Performance Trace
│
▼
進一步追「花在哪裡」
因此除了 renderDuration,Scenario 也會收集 Chrome Performance Trace。

例如先從 Main Thread 觀察:
Main Thread
────────────────────────────
Scripting
████████████
Rendering
██████████████████
Painting
████
再進一步拆解 Browser Rendering:
Rendering
│
├── Recalculate Style
├── Layout
└── Paint
這些資訊才能讓我們開始區分 大量 UI 的成本,主要落在 JavaScript / Vue,還是瀏覽器 Rendering?
如果只測一次,手動開 Chrome DevTools 錄製 Performance Trace 當然可以,但接下來的 Validation 不是只測一次,而是:
Vue 3.5
×
100 / 500 / 1,000 / 5,000
×
Mount / Update
×
多次測量
接著還要用相同流程測不同版本 Vue 3.6,如果全部依靠人工:
操作 UI
↓
開始錄 Trace
↓
觸發 Render
↓
停止 Trace
↓
儲存結果
↓
下一次
測試流程本身就可能開始引入變異,因此後續需要把這個流程自動化,這也是 CDP 出現的原因。
CDP(Chrome DevTools Protocol)可以讓測試程式控制 Chrome,執行固定操作並收集 Performance Trace。
今天沒有進入 CDP 實作,這裡只需要建立一個量測上的前提:
當 Validation 需要大量、重複、可控的 Trace 時,量測流程本身也必須被控制。
後面的 Validation 才會真正用到這套機制。
因此,今天建立的 VDOM Stress Test 最終固定成:
VDOM Stress Test
│
┌────────────┴────────────┐
▼ ▼
Mount Update
│ │
└────────────┬────────────┘
▼
100 / 500 / 1,000 / 5,000
│
┌────────────┴────────────┐
▼ ▼
Render Duration Chrome Trace
│ │
「花多久」 「花在哪」
│
┌──────────┴──────────┐
▼ ▼
Vue / JavaScript Browser Rendering
這個 Scenario 的目的是把原本很模糊的問題 「大量 UI 為什麼會卡?」,轉換成可以重複測量的問題:
當 Render Count 從 100、500、1,000 增加到 5,000 時,Mount 與 Update 的成本如何變化?
再進一步確認:
這些增加的成本,主要發生在 Vue Runtime,還是 Browser Rendering?
最後才有辦法知道 如果換成 Vue 3.6,改善的究竟是哪一層?