Vue3 – 效能與最佳化:Memoization(computed 快取)
簡介
在單頁應用程式 (SPA) 中,隨著畫面與資料的交互日益頻繁,效能 逐漸成為使用者體驗的關鍵因素。Vue 3 為了讓開發者更容易管理大量的計算與渲染工作,引入了 computed 這個「快取」機制。它本質上是一種 memoization(記憶化)技術:只要依賴的資料沒有改變,computed 的結果就會被緩存,下次使用時直接返回先前的值,避免不必要的重新計算。
掌握 computed 的運作原理與最佳化技巧,能讓你的 Vue 應用在 大量資料、複雜運算或頻繁更新 的情境下,仍然保持流暢。本文將從概念切入,透過實作範例說明如何正確使用 computed 快取,並提供常見陷阱與實務建議,幫助你在開發過程中即時提升效能。
核心概念
1. computed 與一般函式的差異
| 項目 | 普通函式 (method) | computed |
|---|---|---|
| 執行時機 | 每次呼叫都會執行 | 只有在依賴的 reactive 資料變化時才重新執行 |
| 快取 | 無快取,除非自行實作 | 內建快取 (memoization) |
| 使用情境 | 需要即時、一次性的計算 | 需要根據 reactive 狀態產生衍生值,且這個值會在多處被使用 |
重點:
computed只會在 依賴變動 時重新計算,這正是 memoization 的核心概念。
2. 依賴收集與觸發機制
Vue 3 採用 Proxy 來攔截對 reactive 物件的讀取與寫入。當 computed getter 內部讀取了某個 reactive 屬性,Vue 會自動把這個屬性加入 依賴集合。之後,只要該屬性被改寫,就會觸發 computed 的重新計算。
import { ref, computed } from 'vue'
const count = ref(0)
// 依賴收集:computed 內部讀取了 count.value
const double = computed(() => {
console.log('計算 double')
return count.value * 2
})
console.log(double.value) // 觸發計算,印出 "計算 double" 與 0
console.log(double.value) // 不會再計算,直接快取結果 0
count.value = 5 // 依賴變動,標記 double 為「dirty」
console.log(double.value) // 再次計算,印出 "計算 double" 與 10
3. computed 的「懶執行」
computed 不會在建立時即刻執行,而是等到第一次讀取 .value 才會跑一次 getter。這意味著如果某個 computed 在整個生命週期中從未被使用,相關的計算根本不會發生,進一步節省資源。
4. 只讀 vs 可寫 computed
- 只讀:僅提供 getter,最常見的使用方式。
- 可寫:同時提供 getter 與 setter,讓外部可以透過
computed直接寫入,背後會觸發對應的 reactive 資料更新。
const firstName = ref('John')
const lastName = ref('Doe')
const fullName = computed({
get() {
return `${firstName.value} ${lastName.value}`
},
set(val) {
const [first, last] = val.split(' ')
firstName.value = first
lastName.value = last
}
})
fullName.value = 'Jane Smith' // 會自動更新 firstName 與 lastName
程式碼範例
以下提供 5 個實用範例,示範在不同情境下如何運用 computed 進行 memoization。
範例 1:列表過濾與排序(大量資料)
import { ref, computed } from 'vue'
const rawList = ref([
/* 假設有 10,000 筆商品資料 */
{ id: 1, name: '蘋果', price: 30, category: '水果' },
// ...
])
const searchKeyword = ref('')
const sortByPriceDesc = ref(false)
// 只在 searchKeyword 或 sortByPriceDesc 改變時重新計算
const filteredAndSorted = computed(() => {
console.log('執行過濾與排序')
const keyword = searchKeyword.value.trim().toLowerCase()
// 1. 先過濾
let result = rawList.value.filter(item =>
item.name.toLowerCase().includes(keyword)
)
// 2. 再排序
result.sort((a, b) =>
sortByPriceDesc.value ? b.price - a.price : a.price - b.price
)
return result
})
說明:若使用
method每次渲染都會跑一次過濾與排序,對 10,000 筆資料的效能會大幅下降。computed只在關鍵字或排序方式變動時重新計算,其他時候直接回傳快取結果。
範例 2:計算大型陣列的統計資訊(避免重複運算)
const numbers = ref(Array.from({ length: 5000 }, () => Math.random() * 100))
const stats = computed(() => {
console.log('計算統計資訊')
const sum = numbers.value.reduce((a, b) => a + b, 0)
const avg = sum / numbers.value.length
const max = Math.max(...numbers.value)
const min = Math.min(...numbers.value)
return { sum, avg, max, min }
})
// 在模板中使用 {{ stats.avg }} 時,只會在 numbers 改變時重新計算
範例 3:可寫 computed - 表單雙向綁定
const startDate = ref('2024-01-01')
const endDate = ref('2024-12-31')
const period = computed({
get() {
return `${startDate.value} ~ ${endDate.value}`
},
set(val) {
const [start, end] = val.split('~').map(s => s.trim())
startDate.value = start
endDate.value = end
}
})
// <input v-model="period"> 會自動同步到 startDate、endDate
範例 4:依賴多個 reactive 物件的計算(使用 toRefs)
import { reactive, toRefs, computed } from 'vue'
const state = reactive({
user: { firstName: 'Alice', lastName: 'Wang' },
locale: 'en'
})
// 把 reactive 轉成 refs,避免在 computed 中直接解構失去響應性
const { user, locale } = toRefs(state)
const greeting = computed(() => {
console.log('產生問候語')
const name = `${user.value.firstName} ${user.value.lastName}`
return locale.value === 'en' ? `Hello, ${name}` : `哈囉,${name}`
})
範例 5:結合 watchEffect 與 computed 的懶加載
import { ref, computed, watchEffect } from 'vue'
const query = ref('vue')
const results = ref([])
// 只在 query 改變時觸發 API 呼叫
const fetchUrl = computed(() => `https://api.example.com/search?q=${encodeURIComponent(query.value)}`)
watchEffect(async () => {
console.log('發送請求至', fetchUrl.value)
const resp = await fetch(fetchUrl.value)
results.value = await resp.json()
})
// 即使在其他地方多次讀取 fetchUrl.value,也只會在 query 改變時重新產生 URL
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
過度依賴 computed |
把所有邏輯都寫在 computed,導致過於複雜、難以維護 |
只將「衍生資料」放在 computed,副作用 (如 API 請求) 應使用 watch / watchEffect |
在 computed 內部直接改變 reactive |
會產生「寫入-讀取」循環,可能導致無限迴圈或錯誤 | computed 必須保持 純函式,若需要改變資料,使用 method 或 watch |
| 解構 reactive 物件導致失去依賴收集 | const { a } = state 會把 a 變成普通變數,computed 無法追蹤 |
使用 toRefs(state) 或直接存取 state.a |
忘記 computed 的懶執行特性 |
認為 computed 會立即執行,導致在建立時就觸發昂貴計算 |
若需要立即執行,可在 setup 中先讀一次 .value,或改用 watchEffect |
使用 computed 包裝大量非同步運算 |
computed 只適合同步計算,非同步會返回 Promise,快取行為不符合預期 |
非同步資料請使用 ref + watch / async setup,或自行實作 memoization 函式 |
最佳實踐清單
- 保持純函式:
computed的 getter 應僅依賴其他 reactive,且不產生副作用。 - 最小化依賴集合:只讀取必要的屬性,避免把整個大型物件一次性放入依賴,減少不必要的重新計算。
- 適度分割:對於複雜的計算,可拆成多個
computed,每個只負責一小塊邏輯,提升可讀性與快取命中率。 - 利用
debugger或console.log:在開發階段加入console.log('計算...'),觀察是否真的在預期時重新計算。 - 結合
watch:當需要在計算結果變化時執行副作用(例如 API 請求、路由跳轉),使用watch監聽computed的值,而非在 getter 內直接呼叫。
實際應用場景
| 場景 | 為什麼使用 computed |
範例 |
|---|---|---|
| 大型表格的分頁、篩選、排序 | 只在使用者變更條件時重新計算子集合,避免每次渲染都遍歷全部資料 | 範例 1 |
| 儀表板統計圖表 | 多個圖表共用相同的原始資料,透過 computed 快取中間統計結果,減少重複計算 |
範例 2 |
| 表單雙向綁定與格式化 | 讓顯示與內部資料保持同步,同時支援使用者直接編輯「合成」欄位 | 範例 3 |
| 多語系動態文字 | 當語系變更時,只重新計算受影響的文字,其他不變的部分保持快取 | 範例 4 |
| 依賴 URL 參數的資料抓取 | URL 只在參數變動時才改變,computed 產生新的請求 URL,watchEffect 負責實際抓取 |
範例 5 |
總結
computed是 Vue 3 提供的 memoization 工具,透過依賴收集與懶執行,能在資料未變時直接回傳快取結果,顯著降低不必要的計算成本。- 正確使用
computed的關鍵在於 保持純函式、最小化依賴、分割複雜邏輯,以及在需要副作用時搭配watch/watchEffect。 - 在 大量資料、頻繁更新、跨組件共享衍生狀態 的情境下,
computed能提供明顯的效能提升,同時讓程式碼更具可讀性與可維護性。
掌握了 computed 的快取機制與最佳化技巧,你的 Vue 3 應用將能在效能與開發體驗之間取得更好的平衡。祝你寫出更快、更穩定的前端程式!