1. 程式人生 > >Vue 應用效能優化指南

Vue 應用效能優化指南

得益於 Vue 的 響應式系統 和 虛擬 DOM 系統 ,Vue 在渲染元件的過程中能自動追蹤資料的依賴,並精確知曉資料更新的時候哪個元件需要重新渲染,渲染之後也會經過虛擬 DOM diff 之後才會真正更新到 DOM 上,Vue 應用的開發者一般不需要做額外的優化工作。

但在實踐中仍然有可能遇到效能問題,下面會介紹一些定位分析 Vue 應用效能問題的方式及一些優化的建議。

<figure>[圖片上傳中...(image-9cb823-1536662035796-15)]

<figcaption></figcaption>

</figure>

整體內容由三部分組成:

  • 如何定位 Vue 應用效能問題
  • Vue 應用執行時效能優化建議
  • Vue 應用載入效能優化建議

1. 如何定位 Vue 應用效能問題

Vue 應用的效能問題可以分為兩個部分,第一部分是執行時效能問題,第二部分是載入效能問題。

和其他 web 應用一樣,定位 Vue 應用效能問題最好的工具是 Chrome Devtool,通過 Performance 工具可以用來錄製一段時間的 CPU 佔用、記憶體佔用、FPS 等執行時效能問題,通過 Network 工具可以用來分析載入效能問題。

<figure>[圖片上傳中...(image-6f0ba7-1536662035796-14)]

<figcaption></figcaption>

</figure>

例如,通過 Performance 工具的 Bottom Up 標籤我們可以看出一段時間內耗時最多的操作,這對於優化 CPU 佔用和 FPS 過低非常有用,可以看出最為耗時的操作發生在哪裡,可以知道具體函式的執行時間,定位到瓶頸之後,我們就可以做一些針對性的優化。

<figure>[圖片上傳中...(image-fba9cb-1536662035796-13)]

<figcaption></figcaption>

</figure>

2. Vue 應用執行時效能優化建議

執行時效能主要關注 Vue 應用初始化之後對 CPU、記憶體、本地儲存等資源的佔用,以及對使用者互動的及時響應。下面是一些有用的優化手段:

2.1 引入生產環境的 Vue 檔案

開發環境下,Vue 會提供很多警告來幫你對付常見的錯誤與陷阱。而在生產環境下,這些警告語句沒有用,反而會增加應用的體積。有些警告檢查還有一些小的執行時開銷

當使用 webpack 或 Browserify 類似的構建工具時,Vue 原始碼會根據 process.env.NODE_ENV 決定是否啟用生產環境模式,預設情況為開發環境模式。在 webpack 與 Browserify 中都有方法來覆蓋此變數,以啟用 Vue 的生產環境模式,同時在構建過程中警告語句也會被壓縮工具去除。

2.2 使用單檔案元件預編譯模板

當使用 DOM 內模板或 JavaScript 內的字串模板時,模板會在執行時被編譯為渲染函式。通常情況下這個過程已經足夠快了,但對效能敏感的應用還是最好避免這種用法

預編譯模板最簡單的方式就是使用單檔案元件——相關的構建設定會自動把預編譯處理好,所以構建好的程式碼已經包含了編譯出來的渲染函式而不是原始的模板字串。

詳細的做法請參閱 預編譯模板

2.3 提取元件的 CSS 到單獨到檔案

當使用單檔案元件時,元件內的 CSS 會以 <style> 標籤的方式通過 JavaScript 動態注入。這有一些小小的執行時開銷,將所有元件的 CSS 提取到同一個檔案可以避免這個問題,也會讓 CSS 更好地進行壓縮和快取。

查閱這個構建工具各自的文件來了解更多:

2.4 利用Object.freeze()提升效能

Object.freeze() 可以凍結一個物件,凍結之後不能向這個物件新增新的屬性,不能修改其已有屬性的值,不能刪除已有屬性,以及不能修改該物件已有屬性的可列舉性、可配置性、可寫性。該方法返回被凍結的物件。

當你把一個普通的 JavaScript 物件傳給 Vue 例項的 data 選項,Vue 將遍歷此物件所有的屬性,並使用 Object.defineProperty 把這些屬性全部轉為 getter/setter,這些 getter/setter 對使用者來說是不可見的,但是在內部它們讓 Vue 追蹤依賴,在屬性被訪問和修改時通知變化。

但 Vue 在遇到像 Object.freeze() 這樣被設定為不可配置之後的物件屬性時,不會為物件加上 setter getter 等資料劫持的方法。參考 Vue 原始碼

Vue observer 原始碼

<figure>[圖片上傳中...(image-aaaf50-1536662035796-12)]

<figcaption></figcaption>

</figure>

2.4.1 效能提升效果對比

在基於 Vue 的一個 big table benchmark 裡,可以看到在渲染一個一個 1000 x 10 的表格的時候,開啟Object.freeze() 前後重新渲染的對比。

big table benchmark

<figure>[圖片上傳中...(image-a22fa3-1536662035796-11)]

<figcaption></figcaption>

</figure>

開啟優化之前

<figure>[圖片上傳中...(image-43e9b7-1536662035796-10)]

<figcaption></figcaption>

</figure>

開啟優化之後

<figure>[圖片上傳中...(image-149bbb-1536662035796-9)]

<figcaption></figcaption>

</figure>

在這個例子裡,使用了 Object.freeze()比不使用快了 4 倍

2.4.2 為什麼Object.freeze() 的效能會更好

不使用Object.freeze() 的CPU開銷

<figure>[圖片上傳中...(image-8b2485-1536662035796-8)]

<figcaption></figcaption>

</figure>

使用 Object.freeze()的CPU開銷

<figure>[圖片上傳中...(image-abd2c5-1536662035796-7)]

<figcaption></figcaption>

</figure>

對比可以看出,使用了 Object.freeze() 之後,減少了 observer 的開銷。

2.4.3 Object.freeze()應用場景

由於 Object.freeze() 會把物件凍結,所以比較適合展示類的場景,如果你的資料屬性需要改變,可以重新替換成一個新的 Object.freeze()的物件。

2.5 扁平化 Store 資料結構

很多時候,我們會發現介面返回的資訊是如下的深層巢狀的樹形結構:

{
  "id": "123",
  "author": {
    "id": "1",
    "name": "Paul"
  },
  "title": "My awesome blog post",
  "comments": [
    {
      "id": "324",
      "commenter": {
        "id": "2",
        "name": "Nicole"
      }
    }
  ]
}
複製程式碼

假如直接把這樣的結構儲存在 store 中,如果想修改某個 commenter 的資訊,我們需要一層層去遍歷找到這個使用者的資訊,同時有可能這個使用者的資訊出現了多次,還需要把其他地方的使用者資訊也進行修改,每次遍歷的過程會帶來額外的效能開銷。

假設我們把使用者資訊在 store 內統一存放成 users[id]這樣的結構,修改和讀取使用者資訊的成本就變得非常低。

你可以手動去把接口裡的資訊通過類似資料的表一樣像這樣存起來,也可以藉助一些工具,這裡就需要提到一個概念叫做 JSON資料規範化(normalize), Normalizr 是一個開源的工具,可以將上面的深層巢狀的 JSON 物件通過定義好的 schema 轉變成使用 id 作為字典的實體表示的物件。

舉個例子,針對上面的 JSON 資料,我們定義 users comments articles 三種 schema:

import {normalize, schema} from 'normalizr';

// 定義 users schema
const user = new schema.Entity('users');

// 定義 comments schema
const comment = new schema.Entity('comments', {
  commenter: user,
});

// 定義 articles schema
const article = new schema.Entity('articles', {
  author: user,
  comments: [comment],
});

const normalizedData = normalize(originalData, article);
複製程式碼

normalize 之後就可以得到下面的資料,我們可以按照這種形式存放在 store 中,之後想修改和讀取某個 id 的使用者資訊就變得非常高效了,時間複雜度降低到了 O(1)。

{
  result: "123",
  entities: {
    "articles": {
      "123": {
        id: "123",
        author: "1",
        title: "My awesome blog post",
        comments: [ "324" ]
      }
    },
    "users": {
      "1": { "id": "1", "name": "Paul" },
      "2": { "id": "2", "name": "Nicole" }
    },
    "comments": {
      "324": { id: "324", "commenter": "2" }
    }
  }
}
複製程式碼

2.6 避免持久化 Store 資料帶來的效能問題

當你有讓 Vue App 離線可用,或者有接口出錯時候進行災備的需求的時候,你可能會選擇把 Store 資料進行持久化,這個時候需要注意以下幾個方面:

2.6.1 持久化時寫入資料的效能問題

Vue 社群中比較流行的 vuex-persistedstate,利用了 store 的 subscribe 機制,來訂閱 Store 資料的 mutation,如果發生了變化,就會寫入 storage 中,預設用的是 localstorage 作為持久化儲存。

也就是說預設情況下每次 commit 都會向 localstorage 寫入資料,localstorage 寫入是同步的,而且存在不小的效能開銷,如果你想打造 60fps 的應用,就必須避免頻繁寫入持久化資料

下面是開發環境下通過 Performance 工具抓取的一個截圖,可以看到出現了一次長達 6s 的卡頓:

6秒鐘的卡頓

<figure>[圖片上傳中...(image-4be20-1536662035795-6)]

<figcaption></figcaption>

</figure>

通過 Bottom-Up 可以看到 setState 佔用了 3241.4ms 的 CPU 執行時間,而 setState 正是在向 Storage 寫入資料。

vuex-persistedstate setState 原始碼

<figure>[圖片上傳中...(image-974603-1536662035795-5)]

<figcaption></figcaption>

</figure>

我們應該儘量減少直接寫入 Storage 的頻率:

  • 多次寫入操作合併為一次,比如採用函式節流或者將資料先快取在記憶體中,最後在一併寫入
  • 只有在必要的時候才寫入,比如只有關心的模組的資料發生變化的時候才寫入

2.6.2 避免持久化儲存的容量持續增長

由於持久化快取的容量有限,比如 localstorage 的快取在某些瀏覽器只有 5M,我們不能無限制的將所有資料都存起來,這樣很容易達到容量限制,同時資料過大時,讀取和寫入操作會增加一些效能開銷,同時記憶體也會上漲。

尤其是將 API 資料進行 normalize 資料扁平化後之後,會將一份資料散落在不同的實體上,下次請求到新的資料也會散落在其他不同的實體上,這樣會帶來持續的儲存增長。

因此,當設計了一套持久化的資料快取策略的時候,同時應該設計舊資料的快取清除策略,例如請求到新資料的時候將舊的實體逐個進行清除。

2.7 優化無限列表效能

如果你的應用存在非常長或者無限滾動的列表,那麼採用 視窗化 的技術來優化效能,只需要渲染少部分割槽域的內容,減少重新渲染元件和建立 dom 節點的時間。

Google 工程師繪製的無限列表設計

<figure>[圖片上傳中...(image-3c6e76-1536662035795-4)]

<figcaption></figcaption>

</figure>

2.8 通過元件懶載入優化超長應用內容初始渲染效能

上面提到的無限列表的場景,比較適合列表內元素非常相似的情況,不過有時候,你的 Vue 應用的超長列表內的內容往往不盡相同,例如在一個複雜的應用的主介面中,整個主介面由非常多不同的模組組成,而使用者看到的往往只有首屏一兩個模組。在初始渲染的時候不可見區域的模組也會執行和渲染,帶來一些額外的效能開銷。

使用元件懶載入在不可見時只需要渲染一個骨架屏,不需要真正渲染元件

<figure>[圖片上傳中...(image-387ea2-1536662035795-3)]

<figcaption></figcaption>

</figure>

你可以對元件直接進行懶載入,對於不可見區域的元件內容,直接不進行載入和初始化,避免初始化渲染執行時的開銷。具體可以參考我們之前的專欄文章 效能優化之元件懶載入: Vue Lazy Component 介紹,瞭解如何做到元件粒度的懶載入。

3. Vue 應用載入效能優化建議

3.1 利用服務端渲染(SSR)和預渲染(Prerender)來優化載入效能

在一個單頁應用中,往往只有一個 html 檔案,然後根據訪問的 url 來匹配對應的路由指令碼,動態地渲染頁面內容。單頁應用比較大的問題是首屏可見時間過長。

單頁面應用顯示一個頁面會發送多次請求,第一次拿到 html 資源,然後通過請求再去拿資料,再將資料渲染到頁面上。而且由於現在微服務架構的存在,還有可能發出多次資料請求才能將網頁渲染出來,每次資料請求都會產生 RTT(往返時延),會導致載入頁面的時間拖的很長。

服務端渲染、預渲染和客戶端渲染的對比

<figure>[圖片上傳中...(image-c66abd-1536662035795-2)]

<figcaption></figcaption>

</figure>

這種情況下可以採用服務端渲染(SSR)和預渲染(Prerender)來提升載入效能,這兩種方案,使用者讀取到的直接就是網頁內容,由於少了節省了很多 RTT(往返時延),同時,還可以對一些資源內聯在頁面,可以進一步提升載入的效能。

可以參考我們的專欄文章 優化向:單頁應用多路由預渲染指南 瞭解如何利用預渲染進行優化。

服務端渲染(SSR)可以考慮使用 Nuxt 或者按照 Vue 官方提供的 Vue SSR 指南來一步步搭建。

3.2 通過元件懶載入優化超長應用內容載入效能

在上面提到的超長應用內容的場景中,通過元件懶載入方案可以優化初始渲染的執行效能,其實,這對於優化應用的載入效能也很有幫助。

元件粒度的懶載入結合非同步元件和 webpack 程式碼分片,可以保證按需載入元件,以及元件依賴的資源、介面請求等,比起通常單純的對圖片進行懶載入,更進一步的做到了按需載入資源。

使用元件懶載入之前的請求瀑布圖

<figure>[圖片上傳中...(image-d77f53-1536662035795-1)]

<figcaption></figcaption>

</figure>

使用元件懶載入之後的請求瀑布圖

<figure>[圖片上傳中...(image-637ab6-1536662035795-0)]

<figcaption></figcaption>

</figure>

使用元件懶載入方案對於超長內容的應用初始化渲染很有幫助,可以減少大量必要的資源請求,縮短渲染關鍵路徑,具體做法請參考我們之前的專欄文章 效能優化之元件懶載入: Vue Lazy Component 介紹

總結

本文總結了 Vue 應用執行時以及載入時的一些效能優化措施,下面做一個回顧和概括:

  • Vue 應用執行時效能優化措施

    • 引入生產環境的 Vue 檔案
    • 使用單檔案元件預編譯模板
    • 提取元件的 CSS 到單獨到檔案
    • 利用Object.freeze()提升效能
    • 扁平化 Store 資料結構
    • 合理使用持久化 Store 資料
    • 元件懶載入
  • Vue 應用載入效能優化措施

    • 服務端渲染 / 預渲染
    • 元件懶載入

文章總結的這些效能優化手段當然不能覆蓋所有的 Vue 應用效能問題,我們也會不斷總結和補充其他問題及優化措施,希望文章中提到這些實踐經驗能給你的 Vue 應用效能優化工作帶來小小的幫助。

</article>