跳轉到內容

效能

概述

Vue被設計成在大多數常見用例中效能良好,無需進行大量手動最佳化。然而,總有一些具有挑戰性的場景需要額外的精細調整。在本節中,我們將討論在Vue應用中關於效能你應該關注什麼。

首先,讓我們討論兩個主要方面:網頁效能

  • 頁面載入效能:應用程式在初次訪問時顯示內容並變得可互動的速度。這通常使用如最大內容渲染時間(LCP)首次輸入延遲(FID)這樣的網路關鍵指標來衡量。

  • 更新效能:應用程式響應使用者輸入時的速度。例如,當用戶在搜尋框中鍵入時列表更新的速度,或者當用戶在單頁面應用程式(SPA)中點選導航連結時頁面切換的速度。

雖然最大化這兩個方面是理想的,但不同的前端架構往往會影響在這些方面獲得期望效能的難易程度。此外,你正在構建的應用程式的型別也極大地影響了你在效能方面應該優先考慮的內容。因此,確保最佳效能的第一步是選擇適合你正在構建的應用程式型別的最合適的架構。

  • 請參閱Vue的使用方式,瞭解您如何以不同的方式利用Vue。

  • Jason Miller在應用全息型別中討論了不同型別的Web應用程式及其各自理想的實現/交付方式。

配置檔案分析

為了提高效能,我們首先需要了解如何衡量它。有很多優秀的工具可以幫助在這方面。

用於分析生產部署的負載效能

用於分析本地開發時的效能

頁面載入最佳化

最佳化頁面載入效能有許多與框架無關的方面 - 檢視此web.dev 指南以獲取全面的概述。在此,我們將主要關注Vue特有的技術。

選擇合適的架構

如果您的用例對頁面載入效能敏感,請避免將其作為純客戶端SPA釋出。您希望伺服器直接傳送包含使用者想要檢視的內容的HTML。純客戶端渲染由於內容載入時間慢而受到限制。這可以透過伺服器端渲染(SSR)靜態站點生成(SSG)來緩解。檢視SSR 指南以瞭解如何使用Vue進行SSR。如果您的應用程式沒有豐富的互動性需求,您也可以使用傳統的後端伺服器來渲染HTML,並在客戶端使用Vue進行增強。

如果您的主應用程式必須是一個SPA,但包含營銷頁面(著陸頁、關於、部落格),請分別釋出它們!理想情況下,您的營銷頁面應作為具有最少JS的靜態HTML透過SSG進行部署。

包大小和搖樹最佳化

提高頁面載入效能的最有效方法之一是釋出更小的JavaScript包。以下是一些在Vue中使用時減少包大小的幾種方法

  • 如果可能的話,使用構建步驟。

    • 許多Vue的API在透過現代構建工具打包時是"搖樹可用的"。例如,如果您不使用內建的<Transition>元件,它將不會包含在最終的生產包中。搖樹也可以從原始碼中移除其他未使用的模組。

    • 在構建步驟中,模板被預編譯,因此我們不需要將Vue編譯器傳送到瀏覽器。這節省了14kb min+gzip壓縮的JavaScript,並避免了執行時編譯的成本。

  • 在引入新依賴時,請小心其大小!在實際應用程式中,膨脹的包通常是由於無意中引入了重量級的依賴項。

    • 如果使用構建步驟,請首選提供ES模組格式且對搖樹友好的依賴項。例如,首選lodash-es而不是lodash

    • 檢查依賴項的大小,並評估其提供的功能是否值得。注意如果依賴項是搖樹友好的,實際的尺寸增加將取決於您從中實際匯入的API。例如,可以使用bundlejs.com等工具進行快速檢查,但使用您的實際構建配置進行測量始終是最準確的。

  • 如果您主要使用Vue進行漸進式增強並希望避免構建步驟,請考慮使用petite-vue(僅6kb)。

程式碼拆分

程式碼拆分是指構建工具將應用程式包拆分為多個更小的塊,這些塊可以按需或並行載入。透過適當的程式碼拆分,可以在頁面載入時立即下載所需的功能,僅在需要時才懶載入額外的塊,從而提高效能。

如Rollup(Vite基於其構建)或webpack之類的打包器可以自動透過檢測ESM動態匯入語法建立拆分塊。

js
// lazy.js and its dependencies will be split into a separate chunk
// and only loaded when `loadLazy()` is called.
function loadLazy() {
  return import('./lazy.js')
}

懶載入最適合用於在初始頁面載入後不需要立即使用的功能。在Vue應用程式中,這可以與Vue的非同步元件功能結合使用,為元件樹建立拆分塊。

js
import { defineAsyncComponent } from 'vue'

// a separate chunk is created for Foo.vue and its dependencies.
// it is only fetched on demand when the async component is
// rendered on the page.
const Foo = defineAsyncComponent(() => import('./Foo.vue'))

對於使用Vue Router的應用程式,強烈建議使用懶載入來處理路由元件。Vue Router對懶載入有顯式支援,與defineAsyncComponent分開。有關更多詳細資訊,請參閱懶載入路由

更新最佳化

Props穩定性

在Vue中,子元件僅在至少一個接收到的屬性發生變化時才會更新。考慮以下示例

template
<ListItem
  v-for="item in list"
  :id="item.id"
  :active-id="activeId" />

<ListItem>元件內部,它使用其idactiveId屬性來確定它是否是當前活動項。雖然這樣可以工作,但問題是每次activeId發生變化時,列表中的所有<ListItem>都必須更新!

理想情況下,只有狀態發生變化的活動項需要更新。我們可以透過將活動狀態計算移動到父元件,並使<ListItem>直接接受一個active屬性來實現這一點。

template
<ListItem
  v-for="item in list"
  :id="item.id"
  :active="item.id === activeId" />

現在,對於大多陣列件,當activeId發生變化時,active屬性將保持不變,因此它們不再需要更新。總的來說,保持傳遞給子元件的屬性儘可能穩定是關鍵。

v-once

v-once是一個內建指令,可用於渲染依賴於執行時資料但永遠不會需要更新的內容。使用它的整個子樹將跳過所有未來的更新。有關更多詳細資訊,請參閱其API參考

v-memo

v-memo是一個內建指令,可用於有條件地跳過大子樹或v-for列表的更新。有關更多詳細資訊,請參閱其API參考

計算穩定性

在Vue 3.4及以上版本中,計算屬性只有在計算值從上一個值更改時才會觸發副作用。例如,以下isEven計算屬性只有在返回值從true變為false或相反時才會觸發副作用。

js
const count = ref(0)
const isEven = computed(() => count.value % 2 === 0)

watchEffect(() => console.log(isEven.value)) // true

// will not trigger new logs because the computed value stays `true`
count.value = 2
count.value = 4

這減少了不必要的副作用觸發,但不幸的是,如果計算建立每個計算的新物件,則不起作用。

js
const computedObj = computed(() => {
  return {
    isEven: count.value % 2 === 0
  }
})

由於每次都會建立一個新的物件,新值在技術上總是與舊值不同。即使isEven屬性保持不變,Vue也無法知道,除非它執行舊值和新值之間的深度比較。這種比較可能代價高昂,而且可能不值得。

相反,我們可以透過手動比較新值與舊值來最佳化這個過程,如果我們知道沒有變化,就條件性地返回舊值

js
const computedObj = computed((oldValue) => {
  const newValue = {
    isEven: count.value % 2 === 0
  }
  if (oldValue && oldValue.isEven === newValue.isEven) {
    return oldValue
  }
  return newValue
})

在遊樂場試一試

請注意,您應該總是在比較和返回舊值之前執行完整的計算,以便每次執行都能收集相同的依賴項。

通用最佳化

以下提示會影響頁面載入和更新效能。

虛擬化大型列表

所有前端應用中最常見的效能問題是渲染大型列表。無論框架有多高效,渲染包含數千個專案的列表都會很慢,因為瀏覽器需要處理的DOM節點數量太多。

然而,我們並不一定需要一開始就渲染所有這些節點。在大多數情況下,使用者的螢幕大小隻能顯示我們大型列表的一小部分。我們可以透過使用**列表虛擬化**技術,僅在大列表中渲染當前位於或接近視口的項,從而顯著提高效能。

實現列表虛擬化並不容易,幸運的是,有一些現有的社群庫可以直接使用

減少大型不可變結構反應性開銷

Vue的反應性系統預設是深層次的。雖然這使得狀態管理直觀,但當資料量很大時,它確實會建立一定程度的開銷,因為每次屬性訪問都會觸發代理陷阱以執行依賴跟蹤。這通常在處理包含100,000+屬性的大型巢狀物件陣列時變得明顯,因此它只會影響非常特定的用例。

Vue確實提供了一個逃生艙,可以透過使用shallowRef()shallowReactive()退出深度反應性。淺API建立的只在最頂層是反應性的狀態,並暴露所有未觸及的巢狀物件。這保持了巢狀屬性訪問的速度,但代價是我們現在必須將所有巢狀物件視為不可變的,並且更新只能透過替換根狀態來觸發

js
const shallowArray = shallowRef([
  /* big list of deep objects */
])

// this won't trigger updates...
shallowArray.value.push(newObject)
// this does:
shallowArray.value = [...shallowArray.value, newObject]

// this won't trigger updates...
shallowArray.value[0].foo = 1
// this does:
shallowArray.value = [
  {
    ...shallowArray.value[0],
    foo: 1
  },
  ...shallowArray.value.slice(1)
]

避免不必要的元件抽象

有時我們可能會建立無渲染元件或高階元件(即透過額外屬性渲染其他元件的元件),以實現更好的抽象或程式碼組織。雖然這沒什麼問題,但請記住,元件例項比普通DOM節點要昂貴得多,並且由於抽象模式建立太多例項將產生效能成本。

請注意,僅減少少量例項不會產生明顯效果,因此如果元件在應用程式中只渲染幾次,請不要擔心。考慮這種最佳化的最佳場景仍然是大型列表。想象一下有100個專案的列表,其中每個專案元件包含許多子元件。在這裡移除一個不必要的元件抽象可能會減少數百個元件例項。

效能已載入