Performance 效能這個東西真的是每個領域的目標
調教效能真的蠻重要~但也是很多知識點的地方
感覺這篇文章一定蠻多內容 Orz...
概述 - Overview
Vue 被設計為在大多數常見使用情境中表現良好,無需過多手動優化。然而,在一些具有挑戰性的場景中,仍需進行額外的微調。本文將探討在 Vue 應用程式中應注意的性能問題。
首先,我們來討論網頁性能的兩個主要方面:
- 頁面加載性能:應用程式在首次訪問時顯示內容並變得可交互的速度。通常使用網頁核心指標如最大內容繪製時間 (Largest Contentful Paint, LCP) 和首次輸入延遲 (First Input Delay, FID) 來衡量。
- 更新性能:應用程式對用戶輸入的響應速度。例如,當用戶在搜索框中輸入時列表更新的速度,或用戶在單頁應用程式 (SPA) 中點擊導航鏈接時頁面切換的速度。
請參考《使用 Vue 的方法》,了解如何以不同方式利用 Vue。
Jason Miller 在《應用程式類型》一文中討論了各類網頁應用程式及其各自的理想實現/交付方式。
性能分析選項 - Profiling Options
要提升性能,首先需要了解如何測量性能。有許多優秀的工具可以幫助完成這項工作:
用於測量生產部署的加載性能:
用於本地開發期間的性能測試:
- Chrome DevTools 性能面板
app.config.performance
可以在 Chrome DevTools 的性能時間軸中啟用 Vue 專屬的性能標記。- Vue DevTools 擴展 也提供性能分析功能。
頁面加載優化 - Page Load Optimizations
優化頁面加載性能有許多與框架無關的方法,請查看這篇 web.dev 指南 以獲取全面的總結。在這裡,我們主要關注 Vue 特有的技術。
選擇合適的架構 - Choosing the Right Architecture
如果您的用例對頁面加載性能敏感,避免將其作為純客戶端 SPA 發布。您希望服務器直接發送包含用戶想要看到的內容的 HTML。純客戶端渲染的缺點是內容顯示速度慢。這可以通過服務端渲染 (SSR) 或靜態網站生成 (SSG) 來緩解。請參閱 SSR 指南 了解如何使用 Vue 執行 SSR。如果您的應用程序不需要豐富的交互性,也可以使用傳統的後端服務器來渲染 HTML,並在客戶端使用 Vue 增強它。
如果您的主要應用程序必須是 SPA,但有一些營銷頁面(登陸頁面、關於頁面、博客),請將它們單獨發布!您的營銷頁面應該理想地作為靜態 HTML 部署,並使用最少的 JS,通過 SSG 來實現。
打包尺寸和瘦身技術 - Bundle Size and Tree-shaking
提升頁面加載性能的最有效方法之一是減少 JavaScript 包的大小。以下是使用 Vue 時減少包大小的一些方法:
- 如果可能,使用構建步驟。
- Vue 的許多 API 如果通過現代構建工具打包是 "tree-shakable" 的。例如,如果您不使用內置的
<Transition>
組件,它將不會包含在最終的生產包中。Tree-shaking 還可以移除源代碼中其他未使用的模塊。 - 使用構建步驟時,模板是預編譯的,因此我們不需要將 Vue 編譯器傳送到瀏覽器。這樣可以節省 14kb 壓縮後的 JavaScript 並避免運行時編譯成本。
- 引入新依賴項時要謹慎!在真實世界的應用中,膨脹的包通常是引入了沉重的依賴項而未意識到它。
- 如果使用構建步驟,首選提供 ES 模塊格式並支持 tree-shaking 的依賴項。例如,首選
lodash-es
而非lodash
。 - 檢查依賴項的大小並評估其提供的功能是否值得。注意如果依賴項支持 tree-shaking,實際的大小增加將取決於您實際導入的 API。可以使用 bundlejs.com 進行快速檢查,但使用實際構建設置進行測量始終是最準確的。
- 如果您主要使用 Vue 進行漸進增強並希望避免構建步驟,請考慮使用
petite-vue
(僅 6kb)。
代碼分割 - Code Splitting
代碼分割是指構建工具將應用程序包分割成多個較小的塊,然後按需或並行加載。通過適當的代碼分割,可以立即下載頁面加載所需的功能,而其他塊僅在需要時延遲加載,從而提升性能。
例如,Rollup(Vite 基於此)或 webpack 可以通過檢測 ESM 動態導入語法自動創建分割塊:
// lazy.js 及其依賴項將被分割成一個單獨的塊
// 並且僅在調用 `loadLazy()` 時加載。
function loadLazy() {
return import('./lazy.js')
}
延遲加載最好用於初始頁面加載後不立即需要的功能。在 Vue 應用程序中,可以結合 Vue 的異步組件功能來為組件樹創建分割塊:
import { defineAsyncComponent } from 'vue'
// Foo.vue 及其依賴項將被創建成一個單獨的塊。
// 當異步組件在頁面上渲染時,它才會按需加載。
const Foo = defineAsyncComponent(() => import('./Foo.vue'))
對於使用 Vue Router 的應用程序,強烈建議對路由組件使用延遲加載。Vue Router 有專門支持延遲加載(lazy loading)的功能,從defineAsyncComponent
分離出來,詳情請參閱 延遲加載路由。
更新優化 - Update Optimizations
Props 穩定性 - Props Stability
在 Vue 中,只有當接收到的 props 至少有一個發生改變時,子組件才會更新。考慮以下示例:
<ListItem
v-for="item in list"
:id="item.id"
:active-id="activeId" />`
在 <ListItem>
組件內部,它使用 id
和 activeId
props 來判斷是否是當前活動的項目。雖然這種方法有效,但問題在於每次 activeId
改變時,列表中的每個 <ListItem>
都必須更新!
理想情況下,只有那些活動狀態改變的項目應該更新。我們可以通過將活動狀態的計算移到父組件中,並使 <ListItem>
直接接受一個 active
prop 來實現:
<ListItem
v-for="item in list"
:id="item.id"
:active="item.id === activeId" />
現在,對於大多數組件來說,當 activeId
改變時,active
prop 將保持不變,因此它們不再需要更新。總的來說,保持傳遞給子組件的 props 盡可能穩定是關鍵。
v-once
v-once
是一個內置指令,可用於渲染依賴運行時數據但不需要更新的內容。使用 v-once
的整個子樹將在所有未來的更新中被跳過。詳細信息請參閱其 API 參考。
v-memo
v-memo
是一個內置指令,可用於有條件地跳過大型子樹或 v-for
列表的更新。詳細信息請參閱其 API 參考。
計算屬性穩定性 - Computed Stability
在 Vue 3.4 及更高版本中,計算屬性僅在其計算值與前一個值發生變化時才會觸發效果。例如,以下 isEven
計算屬性僅在返回值從 true
變為 false
或反之時才會觸發效果:
const count = ref(0)
const isEven = computed(() => count.value % 2 === 0)
watchEffect(() => console.log(isEven.value)) // true
// 不會觸發新的日志,因為計算值保持為 `true`
count.value = 2
count.value = 4
這減少了不必要的效果觸發,但如果計算屬性每次計算都會創建一個新對象,則無法生效:
const computedObj = computed(() => {
return {
isEven: count.value % 2 === 0
}
})
由於每次都創建了一個新對象,技術上新值總是與舊值不同。即使 isEven
屬性保持不變,Vue 也無法知道,除非進行深度比較,而這種比較可能代價高昂且不值得。
相反,我們可以通過手動比較新值與舊值,並在確定沒有變化時有條件地返回舊值來進行優化:
const computedObj = computed((oldValue) => {
const newValue = {
isEven: count.value % 2 === 0
}
if (oldValue && oldValue.isEven === newValue.isEven) {
return oldValue
}
return newValue
})
注意,你應該始終在比較並返回舊值之前完成完整的計算,以便在每次運行時都能收集相同的依賴項。
一般優化 - General Optimizations
以下技巧影響頁面加載和更新性能。
虛擬化大型列表
在所有前端應用中,渲染大型列表是最常見的性能問題之一。無論框架有多高效,由於瀏覽器需要處理的大量 DOM 節點,渲染包含數千項的列表都會變得緩慢。
然而,我們並不一定需要預先渲染所有這些節點。在大多數情況下,用戶的屏幕尺寸只能顯示我們大型列表中的一小部分。我們可以通過列表虛擬化技術來大幅提高性能,這種技術僅渲染當前在視口或接近視口的項目。
實現列表虛擬化並不容易,但幸運的是,有現成的社區庫可以直接使用:
減少大型不可變結構的響應性開銷 - Reduce Reactivity Overhead for Large Immutable Structures
Vue 的響應性系統默認是深度的。雖然這使狀態管理直觀,但當數據量很大時,會產生一定程度的開銷,因為每次訪問屬性都會觸發代理陷阱來進行依賴追蹤。這通常在處理大型深層嵌套對象數組時變得顯著,其中單次渲染需要訪問 100,000 多個屬性,因此僅會影響特定用例。
Vue 提供了一個逃生口來選擇退出深度響應性,使用 shallowRef()
和 shallowReactive()
。淺層 API 創建的狀態僅在根級別是響應的,並且暴露所有未修改的嵌套對象。這使嵌套屬性訪問速度更快,代價是我們現在必須將所有嵌套對象視為不可變的,並且只能通過替換根狀態來觸發更新:
const shallowArray = shallowRef([
/* 大量深層對象列表 */
])
// 這不會觸發更新...
shallowArray.value.push(newObject)
// 這會觸發:
shallowArray.value = [...shallowArray.value, newObject]
// 這不會觸發更新...
shallowArray.value[0].foo = 1
// 這會觸發:
shallowArray.value = [
{
...shallowArray.value[0],
foo: 1
},
...shallowArray.value.slice(1)
]
避免不必要的組件抽象 - Avoid Unnecessary Component Abstractions
有時我們可能會為了更好的抽象或代碼組織,創建無渲染組件或高階組件(即使用額外 props 渲染其他組件的組件)。雖然這本身並沒有問題,但請記住,組件實例比純 DOM 節點昂貴得多,由於抽象模式創建過多組件實例將會產生性能成本。
請注意,減少少量實例不會有顯著效果,因此如果組件在應用中只渲染幾次,不要過於擔心。最佳情況是再次考慮在大型列表中進行此優化。想象一個包含 100 個項目的列表,其中每個項目組件包含許多子組件。在這裡移除一個不必要的組件抽象可能會導致數百個組件實例的減少。
看完這些還是不太清楚,效能差異多少?
要來實際操作看看那些效能工具吧!!!
先記住有這些方法跟工具吧~www