本文 AI 產出,尚未審核

Vue3 – 效能與最佳化:shallowRefmarkRaw 降低追蹤成本


簡介

在 Vue 3 中,Composition API 讓開發者能以更彈性的方式管理狀態與生命週期。雖然 refreactive 為大多數情境提供了便利的響應式機制,但它們在底層會對所有屬性做 深度追蹤(deep tracking)。當資料結構變得龐大或包含大量不需要被 Vue 觀測的物件時,追蹤成本會急速升高,導致 UI 更新變慢、記憶體占用增加,甚至觸發不必要的重新渲染。

shallowRefmarkRaw 兩個 API 正是為了「降低追蹤成本」而設計的。前者只對最外層的值做追蹤,內部的物件保持原樣不被觀測;後者則是告訴 Vue「這個物件永遠不要被轉成 reactive」,讓它直接以原始引用傳遞。掌握這兩個工具,能讓大型專案在效能上更具可預測性,也能避免因過度觀測而產生的效能瓶頸。

以下將從概念說明、實作範例、常見陷阱與最佳實踐、以及實務應用場景,完整介紹如何在 Vue 3 中正確使用 shallowRefmarkRaw 來提升效能。


核心概念

1. 為什麼需要「淺層」或「原始」的參考?

  • 深度追蹤的成本reactive 會遍歷物件的每一層屬性,為每個屬性建立 getter/setter,並在每次變更時觸發依賴追蹤。當物件包含大量屬性、深層嵌套或大量陣列時,建立與維護這些 proxy 的開銷會相當可觀。
  • 不需要被觀測的資料:有些資料只會在初始化時傳入,之後不會再被改變(例如第三方套件的實例、Canvas / WebGL 的 context、或是大型 JSON 快取)。對這類資料進行深度觀測不僅浪費資源,還可能因為不必要的依賴收集導致不預期的重新渲染。

結論:只要資料不會在 Vue 內部被「寫」或「監聽」,就可以使用 shallowRefmarkRaw 讓 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 個實務中常見的使用情境,說明如何正確運用 shallowRefmarkRaw

範例 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 會為 apitheme、以及它們的每個屬性都建立 getter/setter。設定檔通常不會在執行時變更,使用 markRaw 可以減少不必要的 proxy 數量。


範例 5:結合 shallowRefmarkRaw 的動態切換

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。若有部分屬性需要觀測,可把需要觀測的屬性抽離成獨立的 refreactive
忘記在 shallowRef 外層換值 只改變內部屬性不會觸發依賴,可能導致 UI 永遠不更新。 必須在需要觸發更新時整體替換 shallowRef.value,或配合 watchEffect 手動觸發。
混用 reactiveshallowRef 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 應用的 渲染效能記憶體使用率
  • 在實務開發中,先判斷資料的變動頻率與觀測需求,再決定是否使用 shallowRefmarkRaw 或完整的 reactive,才能在效能與可維護性之間取得最佳平衡。

實踐建議:在開發新功能時,先以最簡單的 ref / reactive 實作,確認功能正確後,再根據效能檢測結果,逐步引入 shallowRefmarkRaw 進行優化。透過這樣的漸進式調整,你的 Vue 3 專案將能在保持開發效率的同時,達到更佳的效能表現。