Vue3 – 效能與最佳化:shallowRef 與 markRaw 降低追蹤成本
簡介
在 Vue 3 中,Composition API 讓開發者能以更彈性的方式管理狀態與生命週期。雖然 ref、reactive 為大多數情境提供了便利的響應式機制,但它們在底層會對所有屬性做 深度追蹤(deep tracking)。當資料結構變得龐大或包含大量不需要被 Vue 觀測的物件時,追蹤成本會急速升高,導致 UI 更新變慢、記憶體占用增加,甚至觸發不必要的重新渲染。
shallowRef 與 markRaw 兩個 API 正是為了「降低追蹤成本」而設計的。前者只對最外層的值做追蹤,內部的物件保持原樣不被觀測;後者則是告訴 Vue「這個物件永遠不要被轉成 reactive」,讓它直接以原始引用傳遞。掌握這兩個工具,能讓大型專案在效能上更具可預測性,也能避免因過度觀測而產生的效能瓶頸。
以下將從概念說明、實作範例、常見陷阱與最佳實踐、以及實務應用場景,完整介紹如何在 Vue 3 中正確使用 shallowRef 與 markRaw 來提升效能。
核心概念
1. 為什麼需要「淺層」或「原始」的參考?
- 深度追蹤的成本:
reactive會遍歷物件的每一層屬性,為每個屬性建立 getter/setter,並在每次變更時觸發依賴追蹤。當物件包含大量屬性、深層嵌套或大量陣列時,建立與維護這些 proxy 的開銷會相當可觀。 - 不需要被觀測的資料:有些資料只會在初始化時傳入,之後不會再被改變(例如第三方套件的實例、Canvas / WebGL 的 context、或是大型 JSON 快取)。對這類資料進行深度觀測不僅浪費資源,還可能因為不必要的依賴收集導致不預期的重新渲染。
結論:只要資料不會在 Vue 內部被「寫」或「監聽」,就可以使用
shallowRef或markRaw讓 Vue「跳過」對它的深度追蹤。
2. shallowRef 的工作原理
shallowRef(value) 會建立一個 ref,但它只對 ref 本身(即 .value)做追蹤。若 .value 是物件或陣列,裡面的屬性不會被 Vue 觀測。換句話說,只有當你把整個物件換成另一個物件時,依賴才會重新執行。
import { shallowRef, watch } from 'vue'
const state = shallowRef({ count: 0, list: [] })
watch(state, (newVal, oldVal) => {
console.log('state 被整體替換')
})
// 只會觸發 watch
state.value = { count: 1, list: [] } // ✅ 觸發
state.value.count = 2 // ❌ 不會觸發
state.value.list.push(3) // ❌ 不會觸發
3. markRaw 的工作原理
markRaw(value) 會在 建立 reactive 時「標記」該物件,使 Vue 在遞迴轉換時直接跳過它。被 markRaw 包住的物件不會被 proxy 包裝,也不會成為依賴的來源。這對於 第三方類別實例、DOM/Canvas 元素、或 大型資料結構 非常有用。
import { reactive, markRaw } from 'vue'
class Chart {
constructor(el) { this.el = el }
draw() { /* ... */ }
}
const chart = markRaw(new Chart(document.getElementById('myChart')))
const store = reactive({
title: '報表',
chart // <- 這裡不會被代理
})
// 改變 store.title 仍會觸發更新,chart 本身不會被觀測
store.title = '新報表'
4. shallowRef + markRaw 的組合
在某些情況下,我們需要 「外層可換」且「內部永遠不觀測」。此時可以先 markRaw 再放入 shallowRef:
import { shallowRef, markRaw } from 'vue'
const rawObj = markRaw({ heavy: new Array(10000).fill(0) })
const wrapper = shallowRef(rawObj)
// 只要 wrapper.value 換成別的物件才會觸發依賴
wrapper.value = markRaw({ heavy: new Array(20000).fill(0) })
程式碼範例
以下提供 5 個實務中常見的使用情境,說明如何正確運用 shallowRef 與 markRaw。
範例 1:大型資料快取(不需要觀測)
import { shallowRef } from 'vue'
// 假設從 API 取得 10,000 筆商品資料
const productCache = shallowRef([])
async function loadProducts() {
const res = await fetch('/api/products')
const data = await res.json()
// 只要整體換掉,就會通知使用者
productCache.value = data
}
// 其他組件只要讀取 productCache.value 就能取得最新快取
為什麼使用 shallowRef?
商品資料本身不會在 Vue 內部被修改,只是整批更新。使用shallowRef能避免 Vue 為每筆商品建立 proxy,節省記憶體與初始化時間。
範例 2:第三方 UI 元件(如 Element‑Plus、Ant Design)
import { ref, onMounted, markRaw } from 'vue'
import * as echarts from 'echarts'
export default {
setup() {
const chartInstance = ref(null)
onMounted(() => {
const dom = document.getElementById('myChart')
// 把 echarts 實例標記為 raw,避免被 reactive 包裝
chartInstance.value = markRaw(echarts.init(dom))
})
function updateChart(option) {
if (chartInstance.value) {
chartInstance.value.setOption(option)
}
}
return { updateChart }
}
}
關鍵:
echarts.init回傳的是一個類別實例,裡面含有大量內部狀態。若被 Vue 觀測會導致 proxy 失效、效能下降,所以必須markRaw。
範例 3:Canvas / WebGL Context
import { shallowRef, onMounted } from 'vue'
export default {
setup() {
const gl = shallowRef(null)
onMounted(() => {
const canvas = document.querySelector('#glCanvas')
// 直接存放 WebGLRenderingContext,無需觀測
gl.value = canvas.getContext('webgl')
})
function draw() {
const ctx = gl.value
if (!ctx) return
// ... WebGL 繪圖指令
}
return { draw }
}
}
說明:WebGL context 本身是一個原生物件,Vue 對它做 proxy 不會有任何好處,甚至會觸發錯誤。使用
shallowRef讓 Vue 只追蹤「是否已取得」這個引用。
範例 4:深層嵌套的設定物件(只在初始化時使用)
import { reactive, markRaw } from 'vue'
const rawConfig = {
api: {
baseURL: 'https://api.example.com',
timeout: 5000
},
theme: {
primary: '#409EFF',
secondary: '#909399'
}
}
// 標記整個設定物件為 raw,避免深度遞迴
export const config = reactive({
// 只保留一層 reactive,內部仍是原始物件
settings: markRaw(rawConfig)
})
// 之後在程式中直接讀取 config.settings.api.baseURL
效益:若直接
reactive(rawConfig),Vue 會為api、theme、以及它們的每個屬性都建立 getter/setter。設定檔通常不會在執行時變更,使用markRaw可以減少不必要的 proxy 數量。
範例 5:結合 shallowRef 與 markRaw 的動態切換
import { shallowRef, markRaw } from 'vue'
const heavyObj1 = markRaw({ data: new Array(50000).fill(0) })
const heavyObj2 = markRaw({ data: new Array(80000).fill(0) })
// wrapper 只在外層被觀測,內部永遠保持 raw
const wrapper = shallowRef(heavyObj1)
// 切換時會觸發依賴
function switchData() {
wrapper.value = heavyObj2
}
使用情境:例如在大型資料視覺化工具中,使用者切換不同的資料集。整體切換會觸發 UI 重繪,但不必為每筆資料建立 proxy。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
| 誤把需要觀測的物件當 raw | 若資料在 UI 中會被頻繁修改(例如表單輸入),使用 markRaw 會導致變更不被偵測,畫面不會更新。 |
僅在確定不會被 Vue 內部改寫的情況下使用 markRaw。若有部分屬性需要觀測,可把需要觀測的屬性抽離成獨立的 ref 或 reactive。 |
忘記在 shallowRef 外層換值 |
只改變內部屬性不會觸發依賴,可能導致 UI 永遠不更新。 | 必須在需要觸發更新時整體替換 shallowRef.value,或配合 watchEffect 手動觸發。 |
混用 reactive 與 shallowRef |
把 shallowRef 包在 reactive 內部會讓外層仍然深度追蹤,失去效能優勢。 |
把 shallowRef 直接放在組件的 setup 中,或在 reactive 內只放 markRaw 的物件。 |
在模板中直接解構 shallowRef |
Vue 3 的模板自動解構 .value,但若你在 <script setup> 中手動解構,可能會失去響應式。 |
在模板使用 shallowRef 時直接引用變數,不要在 <script> 中提前解構。 |
把大型陣列直接放入 ref |
ref 本身只追蹤整體,但若你在程式碼中頻繁使用 push/pop,仍會觸發重新渲染(因為 .value 被改變)。 |
若需要頻繁操作陣列,考慮使用 shallowRef + markRaw,或將陣列放在 ref 內部但只在需要時整體替換。 |
最佳實踐小結
- 預先判斷資料是否需要觀測:只對會改變且需要 UI 同步的資料使用
reactive/ref。 - 盡量在資料來源層面使用
markRaw:在建立物件時就標記,避免在後續的reactive轉換中被再次遍歷。 - 僅在外層需要追蹤時使用
shallowRef:例如大型快取、第三方實例、Canvas context。 - 配合
watch/watchEffect:當你只想在 整體切換 時才觸發副作用,使用shallowRef搭配watch可以精準控制。 - 測試效能:使用 Vue DevTools 的「Performance」或 Chrome 的「Performance」面板,觀察是否因大量 proxy 產生不必要的重繪。
實際應用場景
1. 即時資料儀表板
在金融或 IoT 儀表板中,往往會一次性接收大量歷史資料(例如過去 30 天的每秒成交量)。這些歷史資料在 UI 上只會一次性渲染,之後不再改變。將它們存入 shallowRef,只在 切換時間範圍 時整體替換,即可避免每筆資料都被 Vue 觀測,顯著降低初始化時間。
2. 第三方圖表套件(ECharts、Chart.js)
圖表實例內部包含大量 WebGL/Canvas 狀態與快取。若將整個實例放入 reactive,Vue 會嘗試 proxy 所有屬性,導致錯誤或效能崩潰。使用 markRaw(或 shallowRef 包住)可以讓圖表保持原始狀態,僅在需要重新建立圖表時才替換引用。
3. 大型表單資料的「快照」
在表單編輯器中,使用者可能會點擊「保存」或「還原」按鈕,這時會把整個表單資料一次性替換為快照。快照本身不需要被觀測,只需要在切換時觸發更新。shallowRef 能夠完美符合此需求。
4. 遊戲或 3D 引擎的場景物件
Three.js、Babylon.js 等 3D 引擎的場景圖(Scene Graph)包含成千上萬的節點與矩陣運算。將整個場景放入 Vue 的響應式系統會造成巨大的性能損耗。markRaw 可以讓這些物件保持原始引用,並在需要時手動觸發 Vue 更新(例如改變相機位置)。
5. 服務端渲染(SSR)快取
在 SSR 時,我們常會把從 API 抓取的資料暫存於全局變數,以供多個請求共享。這些快取資料在服務端只會被讀取,不會被 Vue 觀測。使用 shallowRef(或直接 markRaw)可以避免在渲染過程中不必要的 proxy 建立,減少記憶體占用。
總結
shallowRef:只追蹤最外層的.value,適合「整體替換」且內部不需要觀測的資料(大型快取、Canvas context、第三方實例)。markRaw:讓 Vue 完全忽略物件的深度轉換,適用於 不會在 Vue 內部被改寫 的物件(第三方套件實例、WebGL context、設定檔等)。- 正確使用這兩個 API 能顯著降低 proxy 建立成本、依賴收集次數,從而提升 Vue 應用的 渲染效能 與 記憶體使用率。
- 在實務開發中,先判斷資料的變動頻率與觀測需求,再決定是否使用
shallowRef、markRaw或完整的reactive,才能在效能與可維護性之間取得最佳平衡。
實踐建議:在開發新功能時,先以最簡單的
ref/reactive實作,確認功能正確後,再根據效能檢測結果,逐步引入shallowRef與markRaw進行優化。透過這樣的漸進式調整,你的 Vue 3 專案將能在保持開發效率的同時,達到更佳的效能表現。