在 Vue 專案裡,隨著功能、組件、狀態管理模組越來越多,初始加載 (initial load)、組件實例化 (component instantiation)、響應性追蹤 (reactivity tracking) 等成本都可能變得沉重。
即使底層 reactivity 跟渲染都非常快,還是可能遇到記憶體峰值、初始化耗時高、首屏載入過重等瓶頸。
今天我們一起「拆 Scope、按需啟動、預熱與延遲初始化」,讓 Vapor 在大型專案中穩健運行。
在大型應用中,多多少少可能會遇到下面這些性能瓶頸:
初次載入體積過大:很多組件/邏輯被打包時就已經載入,即使使用者暫時不會用到它們。
組件實例化成本高:即便組件沒有被渲染/顯示,某些內部 setup
、props
處理、響應性建立等也可能已經預先做了。
響應性追蹤開銷:對所有層級深度的 reactive
/ computed
/ watch
進行依賴追蹤,若資料結構複雜、依賴很多,就可能出現記憶體與 CPU 負荷。
頻繁更新導致多餘 re-render:許多子組件因為 props
或上下文改變,造成不必要的更新。
為了解決這些問題,我們或許可以考慮「延後初始化」與「懶作用域 (lazy scope)」策略,而 Vapor 模式正是 Vue 在新版中為此做的突破性設計。
Vapor 可理解為一種較輕量 / 延遲初始化的組件實例化機制 (或稱模擬實例化)。
它的目的是讓組件在還沒真正需要運行時,不完成完整的 setup / 追蹤流程,以減少初次成本。
以下是 Vapor 模式的一些關鍵設計方向與機制(根據目前公開的預覽與討論):
Lazy props loading(延遲 props 加載):在組件尚未真正「活躍」時,不立即解析 / 建立全部 props 的 reactivity。這可以減少對 props 的追蹤初始化。
延後組件完整實例化:組件被渲染/顯示之前,可能僅建立部分輕量結構,而不把內部所有 reactive
、computed
、watchers
都立即設置,這樣可降低初始化延遲。
作用域 (scope) 延後 / 延遲綁定 (Lazy Scope):在尚未用到某些作用域 (變數、data、computed) 時,延後它們的建立與追蹤。
合併 / 壓縮追蹤 (tracking) 機制:在一些情況下,對深層物件的變動追蹤可以暫緩或懶進行。
與其它效能優化合作:例如代碼拆分 (code splitting)、tree-shaking、lazy hydration、虛擬滾動 (virtual scroll)、v-memo
等。Vue 官方在效能教學裡就強調:很多 Vue API 已經是 tree-shakable,未使用的功能會被移除。
Vapor 模式的目的,是讓未真正顯示或使用的組件維持極小的開銷,等到真正使用時再逐步「激活」。
Lazy Scope(懶作用域)是指:不要在組件 setup 階段就把所有應該可能用到的變數、computed
、watchers
全部建立,而是根據使用情境逐步建立或延後綁定。
拆分作用域:關鍵 vs 非關鍵
把組件中「進入畫面前就必須用到的」部分 (例如 props 驗證、必要狀態) 和「未來互動才會用到的邏輯」分開。
只有在真正需要時才初始化後者。
動態 import / 懶組件 (Async Component)
defineAsyncComponent
或動態 import()
的方式懶載入。使用 v-memo
跳過子樹更新
Vue 的 v-memo
指令能讓某個子樹在依賴不變時跳過重新渲染,適合渲染大量清單時的微優化。
在 Lazy Scope 策略中,可以包裹那些不常變動的區塊,使其不參與頻繁更新。
Props 穩定設計
子組件傳 props
時,盡量讓 props
的值保持穩定,就像不要總傳一個新物件,否則每次 parent 更新都會觸發子組件更新。
在某些情況下,透過計算出來的布林值,例如: isActive: item.id === activeId
比直接傳 activeId
更能避免全量重繪。
shallowRef
/ shallowReactive
對於非常大的資料物件,可考慮用 shallowRef
/ shallowReactive
,這樣 Vue 不會遞迴追蹤整個物件深層變化,只對最外層變動有反應。這是常見的大型資料處理優化策略。
certificates.dev
當真正要讀深層資料變動時,再用特定邏輯取值。
條件初始化 / 延遲監視 (watch
) 建立
有些 watcher 或副作用 (effect) 只在特定條件成立時才真正建立。可以在 setup 裡先不建立 watcher,等某些 flag 變成 true
才建立。
或者把一些副作用延後到 onMounted
、onActivated
等生命週期時才啟動,而不是 setup 階段就全部開啟。
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 則讓昂貴的邏輯延後到真正需要時才啟動,兩者結合後,大型專案能有效降低初始負擔,同時保持靈活與高效。