跳至主要內容
Skip to content

Vue 渲染性能優化

Vue 應用卡頓時,問題不一定在打包或網路。很多時候是畫面更新本身太重:一次渲染太多 DOM、父層更新牽動太多子層、列表 key 不穩定、computed 或 watch 讓更新範圍變大,或第三方套件造成頻繁 layout。

一句話回答:Vue 渲染性能優化的核心,是減少不必要的更新、縮小每次更新影響的範圍,並避免一次建立或操作過多 DOM;具體手段包含穩定 key、合理使用 v-if / v-show、拆分組件、大型列表虛擬化、v-memo / v-once、事件代理,以及避免 DOM 讀寫交錯。


一、 必考觀念

1. 渲染慢通常慢在哪?

一個 Vue 頁面更新,大致會經過:

text
狀態變化
-> 觸發響應式更新
-> 組件 render
-> 產生新 VNode
-> Diff / patch
-> 真實 DOM 更新
-> 瀏覽器 Style / Layout / Paint / Composite

所以「渲染慢」可能來自不同位置:

位置常見原因
響應式觸發狀態設計太粗、深層資料太大、watch 太多
render 階段模板太重、列表太大、方法在模板中反覆計算
Diff 階段key 不穩定、列表大量移動、子樹太大
DOM patch新增、刪除、移動太多真實 DOM
瀏覽器渲染Layout / Paint 成本高、DOM 讀寫交錯

面試時不要只說「Vue Diff 很慢」。Diff 只是其中一段,很多卡頓其實是資料、組件邊界或 DOM 數量造成的。

2. 優化方向:少渲染、小範圍、延後做

渲染性能可以用三句話理解:

text
少渲染:不需要更新的東西不要更新
小範圍:需要更新時,只更新必要子樹
延後做:不在當下需要的東西延後建立或載入

對應到 Vue:

  • 少渲染:v-oncev-memo、computed 快取、避免不必要的父層更新。
  • 小範圍:拆分組件、狀態下沉、穩定 props、穩定 key。
  • 延後做:v-if、異步組件、虛擬列表、非首屏模組延後 mount。

3. 先用工具確認是不是渲染問題

如果使用者說「頁面很卡」,不要直接改程式。

先看:

  • Chrome DevTools Performance 裡是否有 long task。
  • long task 裡是 scripting、rendering 還是 painting。
  • 更新時是否有大量 Layout / Recalculate Style。
  • Vue Devtools 裡哪些組件在頻繁更新。
  • 一次更新涉及多少 DOM 節點。
  • 列表是否一次渲染太多資料。

如果瓶頸在 API 或圖片,渲染優化效果會有限;如果瓶頸在 DOM 節點數或組件更新範圍,才是這篇要處理的主戰場。


二、 縮小更新範圍

1. 拆分組件邊界

父組件狀態變化時,子組件是否需要跟著更新,要看資料依賴與組件邊界。

壞情況是:一個大頁面把所有狀態、列表、彈窗、篩選條件、圖表都放在同一層,任何一個狀態改變都可能讓很大的子樹重新 render。

更好的做法是把更新頻率不同的區塊拆開:

vue
<template>
  <SearchPanel v-model="keyword" />
  <UserTable :users="filteredUsers" />
  <SummaryChart :summary="summary" />
  <EditDialog v-if="editingUser" :user="editingUser" />
</template>

拆分不是為了檔案好看,而是讓每個組件只關心自己需要的資料。

2. 狀態放在更靠近使用者的地方

如果某個狀態只有一個小組件需要,不一定要放在很高的父層。

例如 dropdown open 狀態:

vue
<!-- 不一定需要由整個頁面管理 -->
<DropdownMenu />

DropdownMenu 自己管理 opened,可以避免父層因為開關 dropdown 而重新 render 其他大型區塊。

這種做法常被稱為狀態下沉:狀態放在真正需要它的最近共同位置。

3. 避免模板中反覆做重計算

模板每次 render 都會重新執行其中的表達式或方法呼叫。

不建議:

vue
<li
  v-for="user in users"
  :key="user.id"
>
  {{ formatUserName(user) }}
</li>

如果 formatUserName 很輕,問題不大;但如果它做了排序、過濾、複雜格式化,就會在每次 render 時重複執行。

可以先用 computed 整理:

typescript
const displayUsers = computed(() => {
  return users.value.map(user => ({
    ...user,
    displayName: `${user.department} - ${user.name}`,
  }))
})
vue
<li
  v-for="user in displayUsers"
  :key="user.id"
>
  {{ user.displayName }}
</li>

延伸讀:Vue computed 的實現原理


三、 條件渲染與顯示切換

1. v-if 適合延後建立重型區塊

v-if 條件為 false 時,不會建立對應 DOM 與元件。

vue
<HeavyChart v-if="showChart" />

適合:

  • 初始不顯示。
  • 不常切換。
  • 建立成本高。
  • 不需要保留內部狀態。

例如圖表、富文本編輯器、大型表單、低頻 Modal,都可以用 v-if 避免一開始就 mount。

2. v-show 適合頻繁切換

v-show 會保留 DOM,只切換 display

vue
<TabPanel v-show="activeTab === 'profile'" />

適合:

  • 頻繁切換。
  • 建立成本不高。
  • 需要保留 DOM 或元件狀態。

Tab、Dropdown、Tooltip、展開收合,通常可以考慮 v-show

3. 不要把 v-if / v-show 當絕對規則

常見口訣是:

text
切換少用 v-if
切換多用 v-show

但真實專案還要看:

  • 元件 mount 成本。
  • DOM 子樹大小。
  • 是否需要保留狀態。
  • 是否涉及權限與安全。
  • 初始渲染是否會拖慢首屏。

延伸讀:v-if 與 v-show 的區別


四、 列表渲染優化

1. key 要穩定且能代表資料身份

動態列表要使用穩定 key:

vue
<UserRow
  v-for="user in users"
  :key="user.id"
  :user="user"
/>

不要在會插入、刪除、排序的列表中使用 index 當 key:

vue
<!-- 不建議用在動態列表 -->
<UserRow
  v-for="(user, index) in users"
  :key="index"
  :user="user"
/>

原因是 index 代表位置,不代表資料身份。列表順序一變,DOM 或元件狀態就可能錯位,Diff 也更難正確判斷哪些節點可以復用。

延伸讀:Vue 中 key 的作用Vue 2.x 與 Vue 3.x 渲染器 Diff 算法

2. 先過濾資料,再渲染列表

不建議把 v-ifv-for 寫在同一個節點上:

vue
<!-- 不建議 -->
<UserRow
  v-for="user in users"
  v-if="user.active"
  :key="user.id"
  :user="user"
/>

更好的做法是先用 computed 得到要渲染的資料:

typescript
const activeUsers = computed(() => {
  return users.value.filter(user => user.active)
})
vue
<UserRow
  v-for="user in activeUsers"
  :key="user.id"
  :user="user"
/>

這樣模板更乾淨,也避免每一輪渲染都在節點層做條件判斷。

3. 大型列表要用虛擬列表或分頁

如果一次渲染幾千、幾萬筆資料,Vue Diff 再好也救不了 DOM 數量。

常見策略:

  • 分頁。
  • 分段載入。
  • 虛擬列表。
  • 只渲染可視區域。
  • 降低每一列的組件複雜度。

虛擬列表的核心是:

text
資料有 10000 筆
畫面只顯示 20 筆
DOM 只維持可視區域附近的少量節點
滾動時替換資料與位置

這是在改變渲染策略,不是微調某個 Vue API。

4. 列表中的事件可以考慮事件代理

如果每一列、每個按鈕都綁事件,資料量很大時會增加記憶體與更新成本。

可以把事件放在共同父層:

vue
<ul @click="handleListClick">
  <li
    v-for="user in users"
    :key="user.id"
    :data-id="user.id"
  >
    <button data-action="edit">Edit</button>
    <button data-action="delete">Delete</button>
  </li>
</ul>
typescript
function handleListClick(event: MouseEvent) {
  const target = event.target as HTMLElement
  const action = target.dataset.action
  const item = target.closest('[data-id]') as HTMLElement | null

  if (!action || !item) return

  const id = item.dataset.id

  if (action === 'edit') {
    editUser(id)
  }

  if (action === 'delete') {
    deleteUser(id)
  }
}

不是所有列表都需要事件代理。只有在資料量大、事件很多、更新頻繁時,它才比較值得。


五、 Vue 內建渲染優化能力

1. v-once

v-once 會讓節點只渲染一次,後續更新跳過它。

vue
<section v-once>
  <h1>{{ title }}</h1>
  <p>{{ description }}</p>
</section>

適合:

  • 靜態說明文字。
  • 不會變的條款內容。
  • 初始化後不需要更新的區塊。

但不要濫用。如果資料未來可能變,v-once 會讓畫面不更新,造成維護風險。

2. v-memo

v-memo 是 Vue 3 的優化指令,用來根據依賴陣列跳過子樹更新。

vue
<UserRow
  v-for="user in users"
  :key="user.id"
  v-memo="[user.id, user.selected]"
  :user="user"
/>

只有當 memo 陣列裡的值變了,這個子樹才需要重新更新。

適合:

  • 大列表。
  • 每一列渲染成本高。
  • 大多數列資料不變,只有少數列狀態變。

不適合:

  • 小型列表。
  • 更新邏輯不清楚。
  • 依賴陣列很難維護。

v-memo 是精準工具,不是每個 v-for 都要加。

3. 編譯器已經做了很多優化

Vue 3 的性能不只靠 runtime Diff,也靠 compiler。

編譯器會做:

  • 靜態提升。
  • Patch Flags。
  • Block Tree。
  • 事件快取。

所以很多普通模板不需要手動優化。你真正要注意的是:資料量很大、更新頻率很高、DOM 子樹很重、第三方套件很重的地方。

延伸讀:Vue Compiler 的實現原理


六、 DOM 與瀏覽器渲染成本

1. 避免 DOM 讀寫交錯

壞例子:

typescript
for (const item of items) {
  item.style.width = `${container.offsetWidth}px`
}

這段程式在迴圈裡讀 layout,再寫 style,可能讓瀏覽器反覆同步計算 layout。

比較好的做法:

typescript
const width = container.offsetWidth

for (const item of items) {
  item.style.width = `${width}px`
}

核心原則:

text
先批次讀
再批次寫

延伸讀:瀏覽器渲染管線與 DOM 操作成本

2. 動畫優先使用 transform 與 opacity

會影響 Layout 的屬性比較容易造成卡頓:

  • width
  • height
  • top
  • left
  • margin
  • padding

動畫更常優先考慮:

  • transform
  • opacity

例如:

css
.panel-enter-active,
.panel-leave-active {
  transition: transform 0.2s ease, opacity 0.2s ease;
}

.panel-enter-from,
.panel-leave-to {
  transform: translateY(8px);
  opacity: 0;
}

這不是絕對保證,但作為實務原則很常見。

3. 第三方套件要特別觀察

圖表、地圖、表格、拖拉排序、富文本編輯器常常直接操作 DOM。

整合這些套件時要注意:

  • 是否每次 reactive 變化都重新初始化。
  • 是否可以只更新資料,不重建實例。
  • 是否需要 markRaw 保存第三方實例。
  • 是否會大量測量 DOM 尺寸。
  • 是否需要在 onBeforeUnmount 清理事件與實例。

渲染卡頓不一定是 Vue 本身,很多時候是 Vue 和第三方 DOM 操作互相牽動。


七、 追問題庫

Q1:Vue 頁面更新卡頓,第一步看什麼?

先用 Performance 錄製操作,看卡在 JS、Layout、Paint 還是網路。

如果是 JS long task,再看是組件 render、computed/watch、資料處理或第三方套件。如果是 Layout/Paint,再看 DOM 數量、CSS 與 DOM 讀寫交錯。

Q2:為什麼大列表不能只靠 Diff?

Diff 能減少不必要的 DOM 操作,但不能消除大量 DOM 本身的成本。

如果畫面上真的存在一萬個節點,瀏覽器仍然要管理它們的樣式、佈局、繪製與事件。這時應該用分頁或虛擬列表,減少真實 DOM 數量。

Q3:v-memo 和 computed 差在哪?

computed 快取的是一段衍生資料或計算結果。

v-memo 影響的是模板子樹更新:當依賴陣列沒有變時,Vue 可以跳過這個子樹的更新。

兩者解決的層級不同,一個偏資料計算,一個偏渲染更新。

Q4:拆分組件一定會提升性能嗎?

不一定。

拆分組件主要是為了縮小更新範圍與提升可維護性。如果拆得太碎,也可能增加 props 傳遞、組件層級與理解成本。

比較合理的做法是按「更新頻率、資料依賴、業務邊界」拆分,而不是為了性能把每一行都拆成組件。

Q5:什麼時候需要事件代理?

當列表很大、每列有很多可點擊元素,且事件綁定數量明顯變多時,可以考慮事件代理。

如果只是幾十筆資料,直接在子元素上綁事件通常更清楚,沒必要為了微小收益犧牲可讀性。


八、 實作題

題目:一個 Vue 表格有 10000 筆資料,滾動和勾選都很卡,你怎麼優化?

可以這樣回答:

  1. 先用 Performance 確認卡在 JS、Layout 還是 Paint。
  2. 檢查是否一次渲染 10000 筆 DOM。如果是,優先改成虛擬列表或分頁。
  3. 確認每列是否有穩定 key,不使用 index key。
  4. 檢查勾選狀態是否放在過高父層,導致整張表都更新。
  5. 將每列拆成獨立組件,讓更新集中在被勾選的 row。
  6. 如果是 Vue 3,可以評估在 row 上使用 v-memo
  7. 檢查每列是否有重型 computed、watch 或格式化函數。
  8. 大量按鈕事件可以視情況改成事件代理。
  9. 如果表格套件會測量 DOM,檢查是否造成強制同步佈局。
  10. 優化後再次量測 long task、FPS、更新耗時與 DOM 數量。

這題的關鍵答案是:先減少真實 DOM 數量,再縮小更新範圍,最後才做細節微調。


九、 資深視角

1. 渲染優化要先抓大頭

優先級通常是:

  1. 是否一次渲染太多 DOM。
  2. 是否有穩定 key。
  3. 是否父層更新牽動過大子樹。
  4. 是否有大量重複計算。
  5. 是否有 DOM 讀寫交錯。
  6. 是否真的需要 v-memov-once、手寫 render function。

很多時候,改成虛擬列表的收益遠大於在模板上加很多細碎優化。

2. 不要犧牲可維護性換小幅性能

例如:

  • 到處加 v-memo,但依賴陣列很難維護。
  • 過度事件代理,讓事件來源難追蹤。
  • 為了少一次 render,把狀態拆到很難理解。
  • 過早手寫 render function。

資深工程師要能判斷:這個頁面是否真的值得增加這些複雜度。

3. 渲染性能最後會回到資料結構

如果後端資料沒有穩定 id、列表狀態和資料混在一起、父層每次都重新建立整包 props,前端渲染就很難優化。

所以渲染性能不只是 Vue API 問題,也包含:

  • 資料是否有穩定身份。
  • 狀態是否能局部更新。
  • 列表是否可以分頁或窗口化。
  • UI 是否真的需要同時顯示所有內容。
  • 第三方套件是否支援局部更新。

真正成熟的回答,是從「少操作 DOM」走到「重新設計資料與渲染策略」。


總結

問題回答重點
渲染慢怎麼排查用 Performance / Vue Devtools 看 long task、組件更新、Layout/Paint
怎麼減少更新拆分組件、狀態下沉、computed 快取、v-oncev-memo
列表怎麼優化穩定 key、先過濾再渲染、虛擬列表、分頁、降低 row 複雜度
DOM 成本怎麼控減少節點、批次讀寫、動畫用 transform/opacity
資深取捨先抓大頭,避免為了小幅性能增加大量複雜度

一句完整的面試回答可以是:

Vue 渲染性能優化我會先量測,確認卡在 JS render、Diff、DOM patch 還是瀏覽器 Layout/Paint。具體上會先看是否一次渲染太多 DOM,大列表優先用分頁或虛擬列表;列表更新要有穩定 key,避免 index key;再看父層狀態是否牽動太大子樹,可以透過拆分組件、狀態下沉、computed 快取縮小更新範圍。對靜態區塊可以用 v-once,大型列表少數狀態變化可評估 v-memo。如果有直接 DOM 操作或第三方套件,要避免讀寫交錯造成強制同步佈局。最後會重新量測更新耗時與 long task,確認優化真的有效。

延伸閱讀