Vue3 – 效能與最佳化:key 的最佳化使用
簡介
在 Vue3 中,v-for 搭配 key 是最常見的列表渲染手法。雖然 key 看似只是一個小小的屬性,但它直接影響 虛擬 DOM diff 演算法 的效率與最終產生的實體 DOM 更新成本。若 key 使用不當,列表在資料變動時會觸發大量不必要的重新渲染,導致 UI 卡頓、記憶體浪費,甚至出現狀態錯亂的問題。
本單元將深入探討 key 最佳化 的原理、常見陷阱與實務技巧,讓你在開發中能夠寫出既正確又高效的列表渲染程式碼,提升 Vue3 應用的整體效能。
核心概念
1. 為什麼需要 key
Vue 在比較前後兩個虛擬 DOM 樹時,會以 同層的子節點順序 為基礎進行 diff。若沒有 key,Vue 只能依賴「位置」來判斷哪個節點是「同一個」:
// 沒有 key 時的 diff 流程(簡化版)
prevChildren = [A, B, C]
nextChildren = [B, A, C] // 位置改變
Vue 會把 A、B、C 逐一比對,發現位置不一致,最壞情況下會 銷毀舊節點並重新建立,即使實際資料只是順序調換。加入唯一且穩定的 key 後,Vue 能直接對應到正確的節點:
<div v-for="item in list" :key="item.id">{{ item.name }}</div>
如此一來,只會搬移需要變動的節點,其他節點保持不變,減少 DOM 操作與重新渲染的開銷。
2. key 必須唯一且穩定
- 唯一:同一層級的
key必須彼此不同,否則 Vue 仍會無法正確辨識。 - 穩定:
key的值不應隨渲染次序或臨時變數改變,常用 資料庫主鍵、自增 ID、唯一字串 等。
⚠️ 使用陣列索引 (
index) 作為key是最常見的錯誤,因為索引在資料新增/刪除時會改變,會導致「狀態錯位」的問題。
3. key 與過渡動畫 (Transition)
Vue 的過渡系統會依賴 key 來判斷何時進入 enter、leave 動畫。如果 key 不正確,動畫會錯亂或根本不觸發。
4. 大型列表的虛擬化 (Virtual Scroll)
對於上千筆資料的列表,單純依賴 key 仍無法解決渲染成本。此時可結合 Vue Virtual Scroller 或自行實作虛擬滾動,key 仍是辨識單筆資料的核心。
程式碼範例
範例 1:正確使用唯一 key(最基本)
<template>
<ul>
<li v-for="user in users" :key="user.id">
{{ user.name }}
</li>
</ul>
</template>
<script setup>
import { ref } from 'vue'
const users = ref([
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 3, name: 'Charlie' }
])
</script>
說明:
user.id為資料庫唯一鍵,無論新增或刪除哪筆資料,Vue 都能正確搬移或刪除對應的<li>。
範例 2:避免使用索引作為 key(錯誤示範)
<template>
<ul>
<!-- ❌ 不建議:使用 index 作 key -->
<li v-for="(item, index) in items" :key="index">
{{ item.text }}
</li>
</ul>
</template>
<script setup>
import { ref } from 'vue'
const items = ref([
{ text: 'A' },
{ text: 'B' },
{ text: 'C' }
])
// 假設在開頭插入新元素
items.value.unshift({ text: 'NEW' })
</script>
結果:因為
key依賴索引,插入新元素後原本的 DOM 會被全部重新建立,導致 UI 抖動與可能的輸入框內容遺失。
範例 3:使用組合字串作為 key(適用於多層結構)
<template>
<div v-for="category in categories" :key="category.id">
<h3>{{ category.name }}</h3>
<ul>
<li v-for="product in category.products"
:key="`${category.id}-${product.sku}`">
{{ product.name }}
</li>
</ul>
</div>
</template>
<script setup>
import { ref } from 'vue'
const categories = ref([
{
id: 'c1',
name: '飲料',
products: [
{ sku: 'p01', name: '可樂' },
{ sku: 'p02', name: '果汁' }
]
},
{
id: 'c2',
name: '零食',
products: [
{ sku: 'p11', name: '洋芋片' },
{ sku: 'p12', name: '餅乾' }
]
}
])
</script>
說明:利用
`${category.id}-${product.sku}`組合出全域唯一的key,即使不同類別下的sku可能重複,也不會衝突。
範例 4:在過渡動畫中正確使用 key
<template>
<transition-group name="fade" tag="ul">
<li v-for="msg in messages" :key="msg.id" class="msg-item">
{{ msg.text }}
</li>
</transition-group>
</template>
<script setup>
import { ref } from 'vue'
const messages = ref([
{ id: 1, text: 'Hello' },
{ id: 2, text: 'World' }
])
// 兩秒後加入新訊息
setTimeout(() => {
messages.value.push({ id: 3, text: 'Vue3' })
}, 2000)
</script>
<style scoped>
.fade-enter-active, .fade-leave-active { transition: opacity .5s; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
</style>
重點:
msg.id作為key,讓transition-group能正確辨識哪一筆是「新加入」或「離開」的項目,動畫才會如預期執行。
範例 5:結合虛擬滾動與 key(使用 vue-virtual-scroller)
<template>
<VirtualList
:items="bigList"
:item-height="30"
v-slot="{ item }">
<div class="row" :key="item.id">
{{ item.name }}
</div>
</VirtualList>
</template>
<script setup>
import { ref } from 'vue'
import { VirtualList } from 'vue-virtual-scroller'
const bigList = ref(Array.from({ length: 5000 }, (_, i) => ({
id: i, // 唯一 key
name: `項目 #${i}`
})))
</script>
<style scoped>
.row { line-height: 30px; border-bottom: 1px solid #eee; }
</style>
說明:即使只渲染螢幕可見的幾十筆資料,仍須為每筆資料提供唯一
key,讓虛擬列表在捲動時能正確回收與重用 DOM。
常見陷阱與最佳實踐
| 陷阱 | 可能的影響 | 正確做法 |
|---|---|---|
使用陣列索引 (index) 作 key |
列表插入/刪除時 UI 抖動、狀態錯位 | 使用資料本身的唯一屬性(如 id、uuid) |
key 重複或缺失 |
Vue 無法正確 diff,導致多餘的重繪 | 確保同層級所有 key 唯一 |
key 隨渲染次序變動(例如 Date.now()) |
每次渲染都被視為全新節點,失去效能 | key 必須在整個生命週期內保持 穩定 |
忽略過渡動畫的 key 需求 |
動畫不觸發或錯亂 | 為 <transition-group> 內的每個子項提供唯一 key |
| 大型列表未使用虛擬化 | 初次渲染與滾動時卡頓 | 結合 key 與虛擬滾動技術(如 vue-virtual-scroller) |
最佳實踐小結
- 永遠使用唯一且不變的屬性(
id、uuid)作key。 - 若資料本身沒有唯一欄位,自行生成(如
crypto.randomUUID()),但生成一次後就不要再變。 - 在 嵌套列表 中,使用 組合字串 或 多層唯一鍵,避免跨層衝突。
- 結合 過渡動畫 時,
key更是必不可少。 - 處理 上千筆資料 時,配合虛擬化方案,仍以
key為核心辨識。
實際應用場景
- 動態表格:使用後端回傳的
rowId作key,即使使用者排序、過濾或分頁,Vue 只會更新變動的列。 - 聊天訊息列表:每條訊息都有唯一的
msgId,配合<transition-group>可實現「新訊息淡入」動畫,同時保持已讀狀態不被重置。 - 可拖拉排序(drag‑and‑drop):在
v-for中加入:key="item.id",讓vuedraggable能正確追蹤每個項目,避免拖曳後產生重複或遺失的 DOM。 - 多層級樹狀結構:如檔案總管、目錄樹,使用
${parentId}-${nodeId}作key,保證即使不同分支下的nodeId相同,也不會產生衝突。 - 電商商品列表:結合
vue-virtual-scroller,每件商品的sku為key,即使列表千筆也能流暢滾動與即時篩選。
總結
key看似微小,卻是 Vue3 diff 演算法 的關鍵入口。- 正確的
key必須 唯一且穩定,切勿使用索引或每次渲染都變動的值。 - 在 過渡動畫、嵌套列表、虛擬化 等進階情境下,
key的角色更為重要。 - 掌握上述概念與最佳實踐,能讓你的 Vue3 應用在大型資料、頻繁更新的場景中保持 高效、流暢,同時避免常見的 UI 異常與效能瓶頸。
透過本篇的說明與範例,你已具備在日常開發中正確使用 key 的能力,接下來就把這些技巧套用到自己的專案裡,感受效能提升的即時回饋吧! 🚀