同一個 Reactive Chain,只換 Vue 版本,Runtime 的成本真的變了嗎?
今天只改一件事 Vue 版本 Vue 3.5.40 → Vue 3.6.0-rc.2,其他的 Reactive Chain 的程式碼不動,實驗參數不動,Browser 不換。
// vue@3.5.40 -> vue@3.6.0-rc.2
npm install vue@3.6.0-rc.2
再用相同參數跑 DEPTH = 100 ,且每次連續 Trigger Update 100 次,主要觀察 Update Duration、Computed、Watch、WatchEffect、Render 在 Vue Runtime 的行為有沒有真的改變?
第一次跑完,數字非常醒目:
| Metric | Vue 3.5.40 | Vue 3.6.0-rc.2 |
|---|---|---|
| Average Update Duration | 4.729 ms | 12.188 ms |
| Computed | 10,100 | 10,100 |
| Render | 199 | 199 |
Update Duration +157.7%,不降反增,跟預期落差很大,所以開始回到過去檢視所有流程包含量測系統,避免造成誤會。
重新檢查 Claude Code 的自動化流程時,發現一個非常關鍵的資訊:
document.hidden
↓
true
↓
Chrome Background Tab
↓
Timer / CPU Scheduling
↓
Measurement Noise ↑
也就是說 Claude Code 操作的 Chrome 分頁,其實一直處於 hidden 狀態,這就產生了一個很危險的問題,我們原本想測的是 Vue Runtime Cost,結果實際上測到的可能變成:
Vue Runtime
+
Browser Scheduling
+
Automation Overhead
+
Background Tab Throttling

拿 完全相同的 Vue 3.5.40 重跑,程式沒改、Scenario 沒改,只改變等待方式(setTimeout vs 忙碌輪詢 microtask vs MutationObserver),得到的 Average Update Duration 從 4.7ms 一路跳到 12ms、48ms、64ms、73ms。
| 數值 | 來源與量測情境 |
|---|---|
| 4.729 ms | 第一輪初期測試 (validation-log.md:L18),使用 btn.click() + 固定 setTimeout(10ms) 驅動時量測到的平均值。 |
| 12.188 ms | 第一輪初期測試在 Vue 3.6.0-rc.2 下跑出的單次數值(同樣受 setTimeout 節流與背景分頁排程影響)。 |
| 48.086 ms / 63.942 ms | 改用 MutationObserver 監看 DOM 變化策略重測 Vue 3.5.40 Baseline 時 (validation-log.md:L27-35)的 Trial 2 與 Trial 1 數據。 |
| 64ms ~ 73ms | 嘗試改用「microtask 忙碌輪詢(busy polling)」等不同等待機制時,因為與 Vue 主執行緒產生 CPU 競爭,以及 Chrome 背景分頁(document.hidden === true)被降頻排程所產生的極端雜訊值。 |
同一個 Vue 3.5.40 只是改變量測等待方式,結果可以從 4.7 ms 跑到 73 ms 差到 15×,所以推測問題是瀏覽器自動化驅動腳本(setTimeout vs MutationObserver vs Microtask 輪詢)以及背景分頁節流(Timer Throttling / CPU 排程)引入的環境干擾。
經歷這段我們需要修改自動化腳本,以及在 AI 相關文件寫入這次事件發生問題:單次量測不足以支撐版本比較,透過增加 Trial,觀察結果的波動範圍,再用 Median 作為代表值。
原本:
Click
↓
setTimeout()
↓
讀取結果
改成:
Trigger Update
↓
DOM Mutation
↓
MutationObserver
↓
確認 Update 完成
↓
讀取 Runtime Metrics
這樣做的目的不是讓數字「看起來比較漂亮」,而是盡量避免 Timer 被 Background Tab 節流。
這次兩個版本都使用完全相同的 MutationObserver 方法,而且不是只跑一次,每個版本跑 3 次,取 Median。
| Metric | Vue 3.5.40 | Vue 3.6.0-rc.2 |
|---|---|---|
| # 1 | 63.942 ms | 44.402 ms |
| # 2 | 48.086 ms | 47.943 ms |
| # 3 | 47.042 ms | 47.310 ms |
| Update Duration | 48.086 ms | 47.310 ms |
| Computed | 10,100 | 10,100 |
| Watch | 100 | 100 |
| Render | 199 | 199 |
Vue 3.6 下降 (47.310-48.086)/48.086 = -1.614%,數字看來很好,但是前面提過,當 Vue 3.5 自己重跑,就已經可以出現很大的波動,所以算誤差範圍內。
| Metric | Vue 3.5.40 | Vue 3.6.0-rc.2 |
|---|---|---|
| Update Duration | 48.086 ms | 47.310 ms |
| Computed | 10,100 | 10,100 |
| Watch | 100 | 100 |
| WatchEffect | 101 | 101 |
| Render | 199 | 199 |
DEPTH = 100 各版本連續觸發 100 次 Trigger Update 測試數據顯示,兩邊持平,所以暫時 沒有觀察到可重現、具意義的 Runtime Cost Improvement。
如果只看最後一張表,或許會被理解成 Vue 3.5 ≈ Vue 3.6,沒什麼差!
確實沒有證明 Vue 3.6 變快,但是在談 Framework Performance 之前,我們已經可以確定自己量到的值是有價值的,不是測試環境,那 Vue 3.6 到底改善了什麼?我們下一個 Scenario 繼續找……

flush: 'pre':預設,會在 component DOM update 前執行flush: 'post':DOM 更新後執行flush: 'sync':reactive dependency 改變後立即同步執行
預設 watcher 會 batch,避免同一輪同步 mutation 造成大量 callback。
JavaScript 中的
promise和 Mutation Observer API 都使用微任務佇列去執行它們的回呼函數,但當能夠延遲工作直到當前事件循環過程結束時,也是可以執行微任務的時機。