Vue 應用性能優化指南

64173-bd1d8d0ad301f309.png

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

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

64173-83026f2841daf0bc.png
Vue 應用運行原理

整體內容由三部分組成:

  • 如何定位 Vue 應用性能問題
  • Vue 應用運行時性能優化建議
  • Vue 應用加載性能優化建議

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

Vue 應用的性能問題可以分爲兩個部分,第一部分是運行時性能問題,第二部分是加載性能問題。

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

64173-3fd396ec87391307.png

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

64173-1c8610b571d99b52.png

更多 Chrome Devtool 使用方式請參考使用 Chrome Devtool 定位性能問題 的指南

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 源碼

64173-cf8ad1c23f1e4901.png

2.4.1 性能提升效果對比

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

big table benchmark

64173-cb129aec236b3ced.png

開啓優化之前

64173-6e91813ef9fcb218.png

開啓優化之後

64173-3d2bf741d0121ea8.png

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

2.4.2 爲什麼Object.freeze() 的性能會更好

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

64173-112c4104f80045d1.png

使用 Object.freeze()的CPU開銷

64173-6ad22c45b2050a23.png

對比可以看出,使用了 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" }
    }
  }
}

需要了解更多請參考 normalizr 的文檔 https://github.com/paularmstrong/normalizr

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秒鐘的卡頓

64173-da955401a4194a88.png

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

vuex-persistedstate setState 源碼

64173-f3f6cf2dda0a071f.png

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

  • 多次寫入操作合併爲一次,比如採用函數節流或者將數據先緩存在內存中,最後在一併寫入
  • 只有在必要的時候才寫入,比如只有關心的模塊的數據發生變化的時候才寫入

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

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

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

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

2.7 優化無限列表性能

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

vue-virtual-scroll-listvue-virtual-scroller 都是解決這類問題的開源項目。你也可以參考 Google 工程師的文章Complexities of an Infinite Scroller 來嘗試自己實現一個虛擬的滾動列表來優化性能,主要使用到的技術是 DOM 回收、墓碑元素和滾動錨定。

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

64173-92a90dc8debdba8f.png

2.8 通過組件懶加載優化超長應用內容初始渲染性能

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

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

64173-b1e4d3533ac49930
image

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

3. Vue 應用加載性能優化建議

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

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

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

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

64173-b1339f7c40809bdc.png

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

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

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

3.2 通過組件懶加載優化超長應用內容加載性能

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

組件粒度的懶加載結合異步組件和 webpack 代碼分片,可以保證按需加載組件,以及組件依賴的資源、接口請求等,比起通常單純的對圖片進行懶加載,更進一步的做到了按需加載資源。

使用組件懶加載之前的請求瀑布圖

64173-595046cd9d293f92

使用組件懶加載之後的請求瀑布圖

64173-2f30674b8e0baae2

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

總結

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

  • Vue 應用運行時性能優化措施

    • 引入生產環境的 Vue 文件
    • 使用單文件組件預編譯模板
    • 提取組件的 CSS 到單獨到文件
    • 利用Object.freeze()提升性能
    • 扁平化 Store 數據結構
    • 合理使用持久化 Store 數據
    • 組件懶加載
  • Vue 應用加載性能優化措施

    • 服務端渲染 / 預渲染
    • 組件懶加載

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章