iT邦幫忙

2025 iThome 鐵人賽

DAY 29
0
Vue.js

Vue3.6 的革新:深入理解 Composition API系列 第 29

Day 29: 大型專案 - Vapor + Lazy Scope 策略

  • 分享至 

  • xImage
  •  

在 Vue 專案裡,隨著功能、組件、狀態管理模組越來越多,初始加載 (initial load)、組件實例化 (component instantiation)、響應性追蹤 (reactivity tracking) 等成本都可能變得沉重。

即使底層 reactivity 跟渲染都非常快,還是可能遇到記憶體峰值、初始化耗時高、首屏載入過重等瓶頸。

今天我們一起「拆 Scope、按需啟動、預熱與延遲初始化」,讓 Vapor 在大型專案中穩健運行。

問題與動機


在大型應用中,多多少少可能會遇到下面這些性能瓶頸:

  1. 初次載入體積過大:很多組件/邏輯被打包時就已經載入,即使使用者暫時不會用到它們。

  2. 組件實例化成本高:即便組件沒有被渲染/顯示,某些內部 setupprops 處理、響應性建立等也可能已經預先做了。

  3. 響應性追蹤開銷:對所有層級深度的 reactive / computed / watch 進行依賴追蹤,若資料結構複雜、依賴很多,就可能出現記憶體與 CPU 負荷。

  4. 頻繁更新導致多餘 re-render:許多子組件因為 props 或上下文改變,造成不必要的更新。

為了解決這些問題,我們或許可以考慮「延後初始化」與「懶作用域 (lazy scope)」策略,而 Vapor 模式正是 Vue 在新版中為此做的突破性設計。

Vapor 模式(Vapor Mode):核心理念


Vapor 可理解為一種較輕量 / 延遲初始化的組件實例化機制 (或稱模擬實例化)。
它的目的是讓組件在還沒真正需要運行時,不完成完整的 setup / 追蹤流程,以減少初次成本。

以下是 Vapor 模式的一些關鍵設計方向與機制(根據目前公開的預覽與討論):

  • Lazy props loading(延遲 props 加載):在組件尚未真正「活躍」時,不立即解析 / 建立全部 props 的 reactivity。這可以減少對 props 的追蹤初始化。

  • 延後組件完整實例化:組件被渲染/顯示之前,可能僅建立部分輕量結構,而不把內部所有 reactivecomputedwatchers 都立即設置,這樣可降低初始化延遲。

  • 作用域 (scope) 延後 / 延遲綁定 (Lazy Scope):在尚未用到某些作用域 (變數、data、computed) 時,延後它們的建立與追蹤。

  • 合併 / 壓縮追蹤 (tracking) 機制:在一些情況下,對深層物件的變動追蹤可以暫緩或懶進行。

  • 與其它效能優化合作:例如代碼拆分 (code splitting)、tree-shaking、lazy hydration、虛擬滾動 (virtual scroll)、v-memo 等。Vue 官方在效能教學裡就強調:很多 Vue API 已經是 tree-shakable,未使用的功能會被移除。

Vapor 模式的目的,是讓未真正顯示或使用的組件維持極小的開銷,等到真正使用時再逐步「激活」。

Lazy Scope 策略


Lazy Scope(懶作用域)是指:不要在組件 setup 階段就把所有應該可能用到的變數、computedwatchers 全部建立,而是根據使用情境逐步建立或延後綁定。

  1. 拆分作用域:關鍵 vs 非關鍵

    • 把組件中「進入畫面前就必須用到的」部分 (例如 props 驗證、必要狀態) 和「未來互動才會用到的邏輯」分開。

    • 只有在真正需要時才初始化後者。

  2. 動態 import / 懶組件 (Async Component)

    • 對於那些邏輯較重或依賴大型庫 (例如地圖、圖表、3D 套件等) 的子組件,可以用 defineAsyncComponent 或動態 import() 的方式懶載入。

    Code Splitting

    • 當與 Vapor 模式結合時,子組件的預備初始化也可以延後。
  3. 使用 v-memo 跳過子樹更新

    • Vue 的 v-memo 指令能讓某個子樹在依賴不變時跳過重新渲染,適合渲染大量清單時的微優化。

    • 在 Lazy Scope 策略中,可以包裹那些不常變動的區塊,使其不參與頻繁更新。

  4. Props 穩定設計

    • 子組件傳 props 時,盡量讓 props 的值保持穩定,就像不要總傳一個新物件,否則每次 parent 更新都會觸發子組件更新。

    • 在某些情況下,透過計算出來的布林值,例如: isActive: item.id === activeId 比直接傳 activeId 更能避免全量重繪。

  5. shallowRef / shallowReactive

    • 對於非常大的資料物件,可考慮用 shallowRef / shallowReactive,這樣 Vue 不會遞迴追蹤整個物件深層變化,只對最外層變動有反應。這是常見的大型資料處理優化策略。
      certificates.dev

    • 當真正要讀深層資料變動時,再用特定邏輯取值。

    shallowRef

  6. 條件初始化 / 延遲監視 (watch) 建立

    • 有些 watcher 或副作用 (effect) 只在特定條件成立時才真正建立。可以在 setup 裡先不建立 watcher,等某些 flag 變成 true 才建立。

    • 或者把一些副作用延後到 onMountedonActivated 等生命週期時才啟動,而不是 setup 階段就全部開啟。

  7. Lazy Hydration (如果用 SSR / SSR + CSR 混合策略)

    • 在 SSR 或靜態渲染 (SSG) 應用中,可以將部分組件的 hydration 延後(例如當滾動到可視時才 hydrate)。這樣初始互動性 (Time To Interactive) 可以更快。

    • 透過 Lazy Hydration + Vapor + Lazy Scope,能把整個應用的初始負擔拉得非常輕。

範例


<script setup lang="ts">
import { ref, computed, watch, shallowRef, onMounted } from 'vue'
import { defineAsyncComponent } from 'vue'

// 假設我們有個肥大的子組件
const HeavyChild = defineAsyncComponent(() => import('./HeavyChild.vue'))

// props 是必需的部分,先做最低限度處理
const props = defineProps<{ id: string; flag: boolean }>()
const { id, flag } = toRefs(props)  // 僅做最基礎綁定

// 延遲作用域:某些計算或狀態只在 flag 為真時才初始化
const heavyState = shallowRef<any>(null)
const heavyComputed = ref<any>(null)

function initHeavy() {
  if (!heavyState.value) {
    heavyState.value = { /* some big object / logic */ }
    heavyComputed.value = computed(() => {
      // 基於 heavyState 做運算
      return heavyState.value.someProp + id.value.length
    })
  }
}

// 當 flag 為真時,才初始化
watch(flag, (newVal) => {
  if (newVal) {
    initHeavy()
  }
})

// 假設有個 UI 觸發才用到 heavy 部分
const showHeavy = ref(false)
function onShowHeavy() {
  showHeavy.value = true
  initHeavy()
}

onMounted(() => {
  // 若 flag 初始即為真,也可以在 mounted 時初始化
  if (flag.value) {
    initHeavy()
  }
})
</script>

<template>
  <div>
    <p>id = {{ id }}</p>
    <button @click="onShowHeavy">顯示重部分</button>
    <div v-if="showHeavy">
      <HeavyChild :data="heavyComputed" />
    </div>
  </div>
</template>

優點、挑戰與注意事項


優點 挑戰 / 限制 注意事項
減少初始成本 (初始化、追蹤、實例化等) API 複雜度提升、維護負擔增加 要清楚區分哪段邏輯要延後初始化,避免錯誤或狀態異常
提高大型專案的可擴展性 若過度懶初始化,可能導致點擊/首次交互有延遲 對關鍵 UI 路徑慎選是否要懶初始化
效能提升在記憶體與 CPU 上 與其它優化策略(例如 code splitting、SSR hydration)要搭配 在開發中測試並 profile,避免過早優化
更靈活的組件生命週期控制 若第三方組件不支援此模式,可能有兼容性問題 在框架/生態裡面進行漸進式採用,先在部分組件/頁面測試

小結


Vapor 模式帶來更輕量的組件實例化,Lazy Scope 則讓昂貴的邏輯延後到真正需要時才啟動,兩者結合後,大型專案能有效降低初始負擔,同時保持靈活與高效。

參考資料


  1. vuejs - Bundle Size and Tree-shaking
  2. vuejs - Code Splitting
  3. vuejs - v-memo
  4. vuejs - Performance
  5. certificates.dev - Use Shallow Refs for Large Datasets
  6. Vue3-Lazy-Hydration 使用指南

上一篇
Day 28: SSR / Router - 全鏈路 Vapor 化挑戰
系列文
Vue3.6 的革新:深入理解 Composition API29
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言