本文 AI 產出,尚未審核

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 內部但只在需要時整體替換。

最佳實踐小結

  1. 預先判斷資料是否需要觀測:只對會改變且需要 UI 同步的資料使用 reactive / ref。
  2. 盡量在資料來源層面使用 markRaw:在建立物件時就標記,避免在後續的 reactive 轉換中被再次遍歷。
  3. 僅在外層需要追蹤時使用 shallowRef:例如大型快取、第三方實例、Canvas context。
  4. 配合 watch / watchEffect:當你只想在 整體切換 時才觸發副作用,使用 shallowRef 搭配 watch 可以精準控制。
  5. 測試效能:使用 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 專案將能在保持開發效率的同時,達到更佳的效能表現。