做者: binggg from 迅雷前端javascript
原文地址:Vue 應用性能優化指南css
得益於 Vue 的 響應式系統 和 虛擬 DOM 系統 ,Vue 在渲染組件的過程當中能自動追蹤數據的依賴,並精確知曉數據更新的時候哪一個組件須要從新渲染,渲染以後也會通過虛擬 DOM diff 以後纔會真正更新到 DOM 上,Vue 應用的開發者通常不須要作額外的優化工做。html
但在實踐中仍然有可能遇到性能問題,下面會介紹一些定位分析 Vue 應用性能問題的方式及一些優化的建議。前端
總體內容由三部分組成:vue
Vue 應用的性能問題能夠分爲兩個部分,第一部分是運行時性能問題,第二部分是加載性能問題。java
和其餘 web 應用同樣,定位 Vue 應用性能問題最好的工具是 Chrome Devtool,經過 Performance 工具能夠用來錄製一段時間的 CPU 佔用、內存佔用、FPS 等運行時性能問題,經過 Network 工具能夠用來分析加載性能問題。webpack
例如,經過 Performance 工具的 Bottom Up 標籤咱們能夠看出一段時間內耗時最多的操做,這對於優化 CPU 佔用和 FPS 太低很是有用,能夠看出最爲耗時的操做發生在哪裏,能夠知道具體函數的執行時間,定位到瓶頸以後,咱們就能夠作一些針對性的優化。git
更多 Chrome Devtool 使用方式請參考使用 Chrome Devtool 定位性能問題 的指南github
運行時性能主要關注 Vue 應用初始化以後對 CPU、內存、本地存儲等資源的佔用,以及對用戶交互的及時響應。下面是一些有用的優化手段:web
開發環境下,Vue 會提供不少警告來幫你對付常見的錯誤與陷阱。而在生產環境下,這些警告語句沒有用,反而會增長應用的體積。有些警告檢查還有一些小的運行時開銷。
當使用 webpack 或 Browserify 相似的構建工具時,Vue 源碼會根據 process.env.NODE_ENV 決定是否啓用生產環境模式,默認狀況爲開發環境模式。在 webpack 與 Browserify 中都有方法來覆蓋此變量,以啓用 Vue 的生產環境模式,同時在構建過程當中警告語句也會被壓縮工具去除。
詳細的作法請參閱 生產環境部署
當使用 DOM 內模板或 JavaScript 內的字符串模板時,模板會在運行時被編譯爲渲染函數。一般狀況下這個過程已經足夠快了,但對性能敏感的應用仍是最好避免這種用法。
預編譯模板最簡單的方式就是使用單文件組件——相關的構建設置會自動把預編譯處理好,因此構建好的代碼已經包含了編譯出來的渲染函數而不是原始的模板字符串。
詳細的作法請參閱 預編譯模板
當使用單文件組件時,組件內的 CSS 會以 <style>
標籤的方式經過 JavaScript 動態注入。這有一些小小的運行時開銷,將全部組件的 CSS 提取到同一個文件能夠避免這個問題,也會讓 CSS 更好地進行壓縮和緩存。
查閱這個構建工具各自的文檔來了解更多:
vue-cli
的 webpack 模板已經預先配置好)Object.freeze()
提高性能Object.freeze()
能夠凍結一個對象,凍結以後不能向這個對象添加新的屬性,不能修改其已有屬性的值,不能刪除已有屬性,以及不能修改該對象已有屬性的可枚舉性、可配置性、可寫性。該方法返回被凍結的對象。
當你把一個普通的 JavaScript 對象傳給 Vue 實例的 data
選項,Vue 將遍歷此對象全部的屬性,並使用 Object.defineProperty 把這些屬性所有轉爲 getter/setter,這些 getter/setter 對用戶來講是不可見的,可是在內部它們讓 Vue 追蹤依賴,在屬性被訪問和修改時通知變化。
但 Vue 在遇到像 Object.freeze()
這樣被設置爲不可配置以後的對象屬性時,不會爲對象加上 setter getter 等數據劫持的方法。參考 Vue 源碼
Vue observer 源碼
在基於 Vue 的一個 big table benchmark 裏,能夠看到在渲染一個一個 1000 x 10 的表格的時候,開啓Object.freeze()
先後從新渲染的對比。
big table benchmark
開啓優化以前
開啓優化以後
在這個例子裏,使用了 Object.freeze()
比不使用快了 4 倍
Object.freeze()
的性能會更好不使用Object.freeze()
的CPU開銷
使用 Object.freeze()
的CPU開銷
對比能夠看出,使用了 Object.freeze()
以後,減小了 observer 的開銷。
Object.freeze()
應用場景因爲 Object.freeze()
會把對象凍結,因此比較適合展現類的場景,若是你的數據屬性須要改變,能夠從新替換成一個新的 Object.freeze()
的對象。
不少時候,咱們會發現接口返回的信息是以下的深層嵌套的樹形結構:
{
"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 的文檔 github.com/paularmstro…
當你有讓 Vue App 離線可用,或者有接口出錯時候進行災備的需求的時候,你可能會選擇把 Store 數據進行持久化,這個時候須要注意如下幾個方面:
Vue 社區中比較流行的 vuex-persistedstate,利用了 store 的 subscribe 機制,來訂閱 Store 數據的 mutation,若是發生了變化,就會寫入 storage 中,默認用的是 localstorage 做爲持久化存儲。
也就是說默認狀況下每次 commit 都會向 localstorage 寫入數據,localstorage 寫入是同步的,並且存在不小的性能開銷,若是你想打造 60fps 的應用,就必須避免頻繁寫入持久化數據
下面是開發環境下經過 Performance 工具抓取的一個截圖,能夠看到出現了一次長達 6s 的卡頓:
6秒鐘的卡頓
經過 Bottom-Up 能夠看到 setState 佔用了 3241.4ms 的 CPU 執行時間,而 setState 正是在向 Storage 寫入數據。
vuex-persistedstate setState 源碼
咱們應該儘可能減小直接寫入 Storage 的頻率:
因爲持久化緩存的容量有限,好比 localstorage 的緩存在某些瀏覽器只有 5M,咱們不能無限制的將全部數據都存起來,這樣很容易達到容量限制,同時數據過大時,讀取和寫入操做會增長一些性能開銷,同時內存也會上漲。
尤爲是將 API 數據進行 normalize 數據扁平化後以後,會將一份數據散落在不一樣的實體上,下次請求到新的數據也會散落在其餘不一樣的實體上,這樣會帶來持續的存儲增加。
所以,當設計了一套持久化的數據緩存策略的時候,同時應該設計舊數據的緩存清除策略,例如請求到新數據的時候將舊的實體逐個進行清除。
若是你的應用存在很是長或者無限滾動的列表,那麼採用 窗口化 的技術來優化性能,只須要渲染少部分區域的內容,減小從新渲染組件和建立 dom 節點的時間。
vue-virtual-scroll-list 和 vue-virtual-scroller 都是解決這類問題的開源項目。你也能夠參考 Google 工程師的文章Complexities of an Infinite Scroller 來嘗試本身實現一個虛擬的滾動列表來優化性能,主要使用到的技術是 DOM 回收、墓碑元素和滾動錨定。
Google 工程師繪製的無限列表設計
上面提到的無限列表的場景,比較適合列表內元素很是類似的狀況,不過有時候,你的 Vue 應用的超長列表內的內容每每不盡相同,例如在一個複雜的應用的主界面中,整個主界面由很是多不一樣的模塊組成,而用戶看到的每每只有首屏一兩個模塊。在初始渲染的時候不可見區域的模塊也會執行和渲染,帶來一些額外的性能開銷。
使用組件懶加載在不可見時只須要渲染一個骨架屏,不須要真正渲染組件
你能夠對組件直接進行懶加載,對於不可見區域的組件內容,直接不進行加載和初始化,避免初始化渲染運行時的開銷。具體能夠參考咱們以前的專欄文章 性能優化之組件懶加載: Vue Lazy Component 介紹,瞭解如何作到組件粒度的懶加載。
在一個單頁應用中,每每只有一個 html 文件,而後根據訪問的 url 來匹配對應的路由腳本,動態地渲染頁面內容。單頁應用比較大的問題是首屏可見時間過長。
單頁面應用顯示一個頁面會發送屢次請求,第一次拿到 html 資源,而後經過請求再去拿數據,再將數據渲染到頁面上。並且因爲如今微服務架構的存在,還有可能發出屢次數據請求才能將網頁渲染出來,每次數據請求都會產生 RTT(往返時延),會致使加載頁面的時間拖的很長。
服務端渲染、預渲染和客戶端渲染的對比
這種狀況下能夠採用服務端渲染(SSR)和預渲染(Prerender)來提高加載性能,這兩種方案,用戶讀取到的直接就是網頁內容,因爲少了節省了不少 RTT(往返時延),同時,還能夠對一些資源內聯在頁面,能夠進一步提高加載的性能。
能夠參考咱們的專欄文章 優化向:單頁應用多路由預渲染指南 瞭解如何利用預渲染進行優化。
服務端渲染(SSR)能夠考慮使用 Nuxt 或者按照 Vue 官方提供的 Vue SSR 指南來一步步搭建。
在上面提到的超長應用內容的場景中,經過組件懶加載方案能夠優化初始渲染的運行性能,其實,這對於優化應用的加載性能也頗有幫助。
組件粒度的懶加載結合異步組件和 webpack 代碼分片,能夠保證按需加載組件,以及組件依賴的資源、接口請求等,比起一般單純的對圖片進行懶加載,更進一步的作到了按需加載資源。
使用組件懶加載以前的請求瀑布圖
使用組件懶加載以後的請求瀑布圖
使用組件懶加載方案對於超長內容的應用初始化渲染頗有幫助,能夠減小大量必要的資源請求,縮短渲染關鍵路徑,具體作法請參考咱們以前的專欄文章 性能優化之組件懶加載: Vue Lazy Component 介紹。
本文總結了 Vue 應用運行時以及加載時的一些性能優化措施,下面作一個回顧和歸納:
Vue 應用運行時性能優化措施
Object.freeze()
提高性能Vue 應用加載性能優化措施
文章總結的這些性能優化手段固然不能覆蓋全部的 Vue 應用性能問題,咱們也會不斷總結和補充其餘問題及優化措施,但願文章中提到這些實踐經驗能給你的 Vue 應用性能優化工做帶來小小的幫助。