Vue 3.6 沒有自動消除 Reactive Chain 的成本,因此 AI 如果持續產生 Reactive Anti-pattern,再新的 Runtime 也救不了。
前一天沒有數據支持 Vue 3.6 沒有減少這條 Reactive Graph 所產生的 Effect / Render 執行次數,這讓我開始思考如果 Reactive Graph 本身就是我們建立出來的,Framework 到底能幫我們多少?
我們把 Reactive Update 想成一條路:
Source
↓
Computed / Watch
↓
Component
↓
Browser
如果 Framework 能夠優化 Reactive Runtime,為什麼這條 100 層的 Reactive Chain 仍然維持原本的執行結構?
因為對 Framework 來說,它看到的是一個 合法、而且有明確依賴關係的 Reactive Graph, 每一個節點執行得更有效率,可是…它要怎麼知道其實這條路根本不用蓋這麼長?
Source
↓
computed
↓
computed
↓
watch
↓
watchEffect
↓
computed
↓
Component
過去在 Vue 3.5 和 Vue 3.6 的 Runtime 成本數據沒有出現明顯變化,Computed、Watch、Render 的執行次數也維持一致,所以 我們測到的,會不會不只是 Framework Runtime 的問題,還有 Application 自己建立出來的 Reactive Architecture?
換句話說:
Framework 可以優化「這條路走得多快」,但「這條路為什麼這麼長」,仍然是架構設計的問題。
這件事情放到 AI Coding 時代,就開始變得有趣了,AI 最擅長解決眼前的需求,但是它知道 Reactive Graph 的複雜程度嗎?
以下是跟 AI 合作過程,發現它最容易寫出的程式類型,從這些類型看看它是不是把路走偏了?
watchAI 很喜歡用補丁式思考,例如請他幫忙寫一個電商的購物車與折扣結算頁面邏輯,AI 為了處理「原價、折扣、運費、總金額」的連動關係,寫出了三個獨立的 watch:
import { ref, watch } from 'vue';
// 原始資料 (Data)
const cartItems = ref([{ id: 1, price: 100, qty: 2 }]);
const couponCode = ref('SAVE10');
const isVIP = ref(true);
// 衍生狀態
const subtotal = ref(0);
const discount = ref(0);
const shipping = ref(60);
const total = ref(0);
// ── 災難開始:AI 針對每個需求獨立寫 watch ──
// 需求 1:購物車變動時,更新商品小計
watch(cartItems, (newItems) => {
subtotal.value = newItems.reduce((sum, item) => sum + (item.price * item.qty), 0);
console.log('🔄 watch 1: 小計更新為', subtotal.value);
}, { immediate: true, deep: true });
// 需求 2:當小計或折扣碼、VIP 狀態改變時,更新折扣金額
watch([subtotal, couponCode, isVIP], ([newSub, newCode, newVip]) => {
let baseDiscount = newCode === 'SAVE10' ? 10 : 0;
let vipDiscount = newVip ? 20 : 0;
discount.value = baseDiscount + vipDiscount;
console.log('🔄 watch 2: 折扣更新為', discount.value);
}, { immediate: true });
// 需求 3:當小計與折扣改變時,計算最終運費與總金額
watch([subtotal, discount], ([newSub, newDisc]) => {
// 小計滿 500 免運費
shipping.value = newSub >= 500 ? 0 : 60;
total.value = newSub - newDisc + shipping.value;
console.log('🔄 watch 3: 總金額更新為', total.value);
}, { immediate: true });
一個需求一個 watch,看起來很單純,加起來就很可怕,最後...
cartItems / couponCode / isVIP (Data)
┌──────────┼──────────┐
▼ ▼ ▼
watch 1 watch 2 watch 3
▼ ▼ ▼
計算小計 → 計算折扣 → 計算總金額 (Logic)
其實改用 computed 具有快取特性,且會自動並行追蹤依賴
import { ref, computed } from 'vue';
// 原始資料 (Data)
const cartItems = ref([{ id: 1, price: 100, qty: 2 }]);
const couponCode = ref('SAVE10');
const isVIP = ref(true);
// 乾淨、無副作用的派生狀態 (Logic)
const subtotal = computed(() => {
return cartItems.value.reduce((sum, item) => sum + (item.price * item.qty), 0);
});
const discount = computed(() => {
let baseDiscount = couponCode.value === 'SAVE10' ? 10 : 0;
let vipDiscount = isVIP.value ? 20 : 0;
return baseDiscount + vipDiscount;
});
const shipping = computed(() => subtotal.value >= 500 ? 0 : 60);
const total = computed(() => subtotal.value - discount.value + shipping.value);
前面說改用 computed ,它又可能走向另一個極端,結果 AI 接受指令遵循「不要用 watch」的指令,像包洋蔥一樣一層包一層,發現另一個常見情況:
user
↓
computed
↓
computed
↓
computed
↓
computed
例如:姓名、VIP 稱號、會員等級、畫面標題,每一層都包成 computed,但如果這些只是單純的資料整理,其實沒有必要全部變成 Reactive Boundary。
可以把中間邏輯留在同一個 computed 裡:
const finalTitle = computed(() => {
const fullName = `${user.lastName}${user.firstName}`
const vipText = user.vipLevel > 2 ? '[VIP]' : '[一般會員]'
return `${vipText} ${fullName}`
})
前面兩個例子都出現 原本只需要一份資料,最後卻建立了更多 Reactive 節點,專案愈做愈大的時候會不知道源頭在哪,於是變成能動的程式碼就不要輕易改變它!
Pinia
↓
discountTotal
↓
watch
↓
localFinalPrice
↓
UI
一開始看起來沒有問題,但 localFinalPrice 一旦可以被 UI 修改,資料流就變成:
Pinia
↓
discountTotal
↓
watch
↓
localFinalPrice
↕
UI
這時候,除了 Pinia 還有 UI 也可以改變這個值,也就是當如使用者輸入 750,Pinia 更新後變成 780,watch 又會把 780 寫回 localFinalPrice。
結果就是你蓋我我蓋你,這不是 Vue Runtime 的問題,而是我們為了「同步」,建立了第二份可以被修改的 State。

所以 資料已經有明確的 Source,就不要為了方便而複製一份可以被獨立修改的 Reactive State。
比較乾淨的資料流會是:
Source
│
↓
Derived State
│
↓
UI
│
↓
Action
│
└────→ Source
這樣 UI 看到的是 Source 的衍生結果;需要修改時,再透過 Action 回到真正的 Source。
通常我們大多時候下 Prompt 都是「我要什麼功能」,或是把 PM 需求、Open API 直接複製貼上,這樣很容易遇到上面提到的問題,為了避免最後 Code Review 要另外燒 token,可以先告訴它:
產生 Vue Composition API 程式碼。
請遵循以下原則:
✓ 保持單一資料來源(single source of truth, SSOT)
✓ 優先使用單層 computed,避免建立 computed chain
✓ 僅在必要時使用 watch
✓ 避免建立不必要的響應式狀態(ref、reactive)
✓ 每個 watch 都必須說明存在的目的
【 AI 導向的 Vue 響應式優化工作流 】
🎯 1. AI Prompt (輸入規範)
┌───────────────────────────────────────────┐
│ Checklist 規則注入: ─ │
│ [✓] One source of truth (單一真相) │
│ [✗] No computed chains (禁止連續計算) │
│ [⚠️] Minimize/Explain watch (減少監聽) │
└───────────────────────────────────────────┘
│
▼
🤖 2. Generated Code (程式碼生成)
┌───────────────────────────────────────────┐
│ 📄 Vue 3 Component (Composition API) │
│ ├── state ──► computed (乾淨的單向流) │
│ └── watch ──► [說明文字] (必要才使用) │
└───────────────────────────────────────────┘
│
▼
🔍 3. Review (雙重審查機制)
┌───────────────────────────────────────────┐
│ 🤖 AI 自檢:是否落實 Explain every watch? │
│ 👥 人工審查:檢查有無進流 Reactive Chain? │
└───────────────────────────────────────────┘
│
┌───────┴───────┐
▼ ▼
[ ❌ 失敗 ] [ ✔️ 成功 ]
退回修正 合併產出
產生出來的程式碼提交前,也會做最後以下確認再提交 MR。
1. 這個 watch 可以拿掉嗎?
2. 這個 computed 真的需要獨立存在嗎?
3. State Source 只有一個嗎?
4. 有沒有重複 State?
5. Reactive Chain 超過三層了嗎?
這十天的目標,並不是證明 Vue 3.5 或 Vue 3.6 哪一個比較快,而是回答一個更重要的問題:
哪些效能問題可以交給 Framework 解決?哪些問題仍然需要 RD 設計來避免?
到了 AI Coding 時代,AI 可以快速 產生更多 Code,但是沒有好的邊界規範,產生的 Reactive Architecture 極有可能讓未來的自己很想殺掉現在的自己!