Vue 性能優(yōu)化

得益于 Vue 的?響應(yīng)式系統(tǒng)?和?虛擬 DOM 系統(tǒng)?,Vue 在渲染組件的過(guò)程中能自動(dòng)追蹤數(shù)據(jù)的依賴,并精確知曉數(shù)據(jù)更新的時(shí)候哪個(gè)組件需要重新渲染,渲染之后也會(huì)經(jīng)過(guò)虛擬 DOM diff 之后才會(huì)真正更新到 DOM 上,Vue 應(yīng)用的開(kāi)發(fā)者一般不需要做額外的優(yōu)化工作。

但在實(shí)踐中仍然有可能遇到性能問(wèn)題,下面會(huì)介紹一些定位分析 Vue 應(yīng)用性能問(wèn)題的方式及一些優(yōu)化的建議。

整體內(nèi)容由三部分組成:

1、如何定位 Vue 應(yīng)用性能問(wèn)題

2、Vue 應(yīng)用運(yùn)行時(shí)性能優(yōu)化建議

3、Vue 應(yīng)用加載性能優(yōu)化建議


1. 如何定位 Vue 應(yīng)用性能問(wèn)題

Vue 應(yīng)用的性能問(wèn)題可以分為兩個(gè)部分,第一部分是運(yùn)行時(shí)性能問(wèn)題,第二部分是加載性能問(wèn)題。

和其他 web 應(yīng)用一樣,定位 Vue 應(yīng)用性能問(wèn)題最好的工具是 Chrome Devtool,通過(guò) Performance 工具可以用來(lái)錄制一段時(shí)間的 CPU 占用、內(nèi)存占用、FPS 等運(yùn)行時(shí)性能問(wèn)題,通過(guò) Network 工具可以用來(lái)分析加載性能問(wèn)題。

例如,通過(guò) Performance 工具的 Bottom Up 標(biāo)簽我們可以看出一段時(shí)間內(nèi)耗時(shí)最多的操作,這對(duì)于優(yōu)化 CPU 占用和 FPS 過(guò)低非常有用,可以看出最為耗時(shí)的操作發(fā)生在哪里,可以知道具體函數(shù)的執(zhí)行時(shí)間,定位到瓶頸之后,我們就可以做一些針對(duì)性的優(yōu)化。

2. Vue 應(yīng)用運(yùn)行時(shí)性能優(yōu)化建議

運(yùn)行時(shí)性能主要關(guān)注 Vue 應(yīng)用初始化之后對(duì) CPU、內(nèi)存、本地存儲(chǔ)等資源的占用,以及對(duì)用戶交互的及時(shí)響應(yīng)。下面是一些有用的優(yōu)化手段:


2.1 引入生產(chǎn)環(huán)境的 Vue 文件

開(kāi)發(fā)環(huán)境下,Vue 會(huì)提供很多警告來(lái)幫你對(duì)付常見(jiàn)的錯(cuò)誤與陷阱。而在生產(chǎn)環(huán)境下,這些警告語(yǔ)句沒(méi)有用,反而會(huì)增加應(yīng)用的體積。有些警告檢查還有一些小的運(yùn)行時(shí)開(kāi)銷(xiāo)

當(dāng)使用 webpack 或 Browserify 類(lèi)似的構(gòu)建工具時(shí),Vue 源碼會(huì)根據(jù) process.env.NODE_ENV 決定是否啟用生產(chǎn)環(huán)境模式,默認(rèn)情況為開(kāi)發(fā)環(huán)境模式。在 webpack 與 Browserify 中都有方法來(lái)覆蓋此變量,以啟用 Vue 的生產(chǎn)環(huán)境模式,同時(shí)在構(gòu)建過(guò)程中警告語(yǔ)句也會(huì)被壓縮工具去除。


2.2 使用單文件組件預(yù)編譯模板

當(dāng)使用 DOM 內(nèi)模板或 JavaScript 內(nèi)的字符串模板時(shí),模板會(huì)在運(yùn)行時(shí)被編譯為渲染函數(shù)。通常情況下這個(gè)過(guò)程已經(jīng)足夠快了,但對(duì)性能敏感的應(yīng)用還是最好避免這種用法

預(yù)編譯模板最簡(jiǎn)單的方式就是使用單文件組件——相關(guān)的構(gòu)建設(shè)置會(huì)自動(dòng)把預(yù)編譯處理好,所以構(gòu)建好的代碼已經(jīng)包含了編譯出來(lái)的渲染函數(shù)而不是原始的模板字符串。


2.3 提取組件的 CSS 到單獨(dú)到文件

當(dāng)使用單文件組件時(shí),組件內(nèi)的 CSS 會(huì)以?<style>?標(biāo)簽的方式通過(guò) JavaScript 動(dòng)態(tài)注入。這有一些小小的運(yùn)行時(shí)開(kāi)銷(xiāo),將所有組件的 CSS 提取到同一個(gè)文件可以避免這個(gè)問(wèn)題,也會(huì)讓 CSS 更好地進(jìn)行壓縮和緩存。

查閱這個(gè)構(gòu)建工具各自的文檔來(lái)了解更多:

1、webpack + vue-loader (vue-cli?的 webpack 模板已經(jīng)預(yù)先配置好)

2、Browserify + vueify

3、Rollup + rollup-plugin-vue


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

Object.freeze()?可以凍結(jié)一個(gè)對(duì)象,凍結(jié)之后不能向這個(gè)對(duì)象添加新的屬性,不能修改其已有屬性的值,不能刪除已有屬性,以及不能修改該對(duì)象已有屬性的可枚舉性、可配置性、可寫(xiě)性。該方法返回被凍結(jié)的對(duì)象。

當(dāng)你把一個(gè)普通的 JavaScript 對(duì)象傳給 Vue 實(shí)例的 ?data? 選項(xiàng),Vue 將遍歷此對(duì)象所有的屬性,并使用 ?Object.defineProperty? 把這些屬性全部轉(zhuǎn)為 getter/setter,這些 getter/setter 對(duì)用戶來(lái)說(shuō)是不可見(jiàn)的,但是在內(nèi)部它們讓 Vue 追蹤依賴,在屬性被訪問(wèn)和修改時(shí)通知變化。

但 Vue 在遇到像?Object.freeze()?這樣被設(shè)置為不可配置之后的對(duì)象屬性時(shí),不

會(huì)為對(duì)象加上 setter getter 等數(shù)據(jù)劫持的方法。參考 Vue 源碼


Vue observer 源碼

2.4.1 性能提升效果對(duì)比

在基于 Vue 的一個(gè) big table benchmark 里,可以看到在渲染一個(gè)一個(gè) 1000 x 10 的表格的時(shí)候,開(kāi)啟Object.freeze()?前后重新渲染的對(duì)比。

big table benchmark

開(kāi)啟優(yōu)化之前

開(kāi)啟優(yōu)化之后

在這個(gè)例子里,使用了?Object.freeze()比不使用快了 4 倍

2.4.2 為什么Object.freeze()?的性能會(huì)更好

不使用Object.freeze()?的CPU開(kāi)銷(xiāo)

使用?Object.freeze()的CPU開(kāi)銷(xiāo)

對(duì)比可以看出,使用了?Object.freeze()?之后,減少了 observer 的開(kāi)銷(xiāo)。

2.4.3?Object.freeze()應(yīng)用場(chǎng)景

由于?Object.freeze()?會(huì)把對(duì)象凍結(jié),所以比較適合展示類(lèi)的場(chǎng)景,如果你的數(shù)據(jù)屬性需要改變,可以重新替換成一個(gè)新的?Object.freeze()的對(duì)象。

然后附上黃軼大大更加好的優(yōu)化方案:


2.5 扁平化 Store 數(shù)據(jù)結(jié)構(gòu)

很多時(shí)候,我們會(huì)發(fā)現(xiàn)接口返回的信息是如下的深層嵌套的樹(shù)形結(jié)構(gòu):

{

??"id":?"123",

??"author":?{

????"id":?"1",

????"name":?"Paul"

??},

??"title":?"My?awesome?blog?post",

??"comments":?[

????{

??????"id":?"324",

??????"commenter":?{

????????"id":?"2",

????????"name":?"Nicole"

??????}

????}

??]

}

假如直接把這樣的結(jié)構(gòu)存儲(chǔ)在 store 中,如果想修改某個(gè) commenter 的信息,我們需要一層層去遍歷找到這個(gè)用戶的信息,同時(shí)有可能這個(gè)用戶的信息出現(xiàn)了多次,還需要把其他地方的用戶信息也進(jìn)行修改,每次遍歷的過(guò)程會(huì)帶來(lái)額外的性能開(kāi)銷(xiāo)。

假設(shè)我們把用戶信息在 store 內(nèi)統(tǒng)一存放成?users[id]這樣的結(jié)構(gòu),修改和讀取用戶信息的成本就變得非常低。

你可以手動(dòng)去把接口里的信息通過(guò)類(lèi)似數(shù)據(jù)的表一樣像這樣存起來(lái),也可以借助一些工具,這里就需要提到一個(gè)概念叫做?JSON數(shù)據(jù)規(guī)范化(normalize), Normalizr 是一個(gè)開(kāi)源的工具,可以將上面的深層嵌套的 JSON 對(duì)象通過(guò)定義好的 schema 轉(zhuǎn)變成使用 id 作為字典的實(shí)體表示的對(duì)象。

舉個(gè)例子,針對(duì)上面的 JSON 數(shù)據(jù),我們定義?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 之后就可以得到下面的數(shù)據(jù),我們可以按照這種形式存放在 store 中,之后想修改和讀取某個(gè) id 的用戶信息就變得非常高效了,時(shí)間復(fù)雜度降低到了 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 數(shù)據(jù)帶來(lái)的性能問(wèn)題

當(dāng)你有讓 Vue App 離線可用,或者有接口出錯(cuò)時(shí)候進(jìn)行災(zāi)備的需求的時(shí)候,你可能會(huì)選擇把 Store 數(shù)據(jù)進(jìn)行持久化,這個(gè)時(shí)候需要注意以下幾個(gè)方面:


2.6.1 持久化時(shí)寫(xiě)入數(shù)據(jù)的性能問(wèn)題

Vue 社區(qū)中比較流行的 vuex-persistedstate,利用了 store 的 subscribe 機(jī)制,來(lái)訂閱 Store 數(shù)據(jù)的 mutation,如果發(fā)生了變化,就會(huì)寫(xiě)入 storage 中,默認(rèn)用的是 localstorage 作為持久化存儲(chǔ)。

也就是說(shuō)默認(rèn)情況下每次 commit 都會(huì)向 localstorage 寫(xiě)入數(shù)據(jù),localstorage 寫(xiě)入是同步的,而且存在不小的性能開(kāi)銷(xiāo),如果你想打造 60fps 的應(yīng)用,就必須避免頻繁寫(xiě)入持久化數(shù)據(jù)

下面是開(kāi)發(fā)環(huán)境下通過(guò) Performance 工具抓取的一個(gè)截圖,可以看到出現(xiàn)了一次長(zhǎng)達(dá) 6s 的卡頓:

6秒鐘的卡頓

通過(guò) Bottom-Up 可以看到 setState 占用了 3241.4ms 的 CPU 執(zhí)行時(shí)間,而 setState 正是在向 Storage 寫(xiě)入數(shù)據(jù)。

vuex-persistedstate setState 源碼


我們應(yīng)該盡量減少直接寫(xiě)入 Storage 的頻率:

1、多次寫(xiě)入操作合并為一次,比如采用函數(shù)節(jié)流或者將數(shù)據(jù)先緩存在內(nèi)存中,最后在一并寫(xiě)入

2、只有在必要的時(shí)候才寫(xiě)入,比如只有關(guān)心的模塊的數(shù)據(jù)發(fā)生變化的時(shí)候才寫(xiě)入


2.6.2 避免持久化存儲(chǔ)的容量持續(xù)增長(zhǎng)

由于持久化緩存的容量有限,比如 localstorage 的緩存在某些瀏覽器只有 5M,我們不能無(wú)限制的將所有數(shù)據(jù)都存起來(lái),這樣很容易達(dá)到容量限制,同時(shí)數(shù)據(jù)過(guò)大時(shí),讀取和寫(xiě)入操作會(huì)增加一些性能開(kāi)銷(xiāo),同時(shí)內(nèi)存也會(huì)上漲。

尤其是將 API 數(shù)據(jù)進(jìn)行 normalize 數(shù)據(jù)扁平化后之后,會(huì)將一份數(shù)據(jù)散落在不同的實(shí)體上,下次請(qǐng)求到新的數(shù)據(jù)也會(huì)散落在其他不同的實(shí)體上,這樣會(huì)帶來(lái)持續(xù)的存儲(chǔ)增長(zhǎng)。

因此,當(dāng)設(shè)計(jì)了一套持久化的數(shù)據(jù)緩存策略的時(shí)候,同時(shí)應(yīng)該設(shè)計(jì)舊數(shù)據(jù)的緩存清除策略,例如請(qǐng)求到新數(shù)據(jù)的時(shí)候?qū)⑴f的實(shí)體逐個(gè)進(jìn)行清除。


2.7 優(yōu)化無(wú)限列表性能

如果你的應(yīng)用存在非常長(zhǎng)或者無(wú)限滾動(dòng)的列表,那么采用?窗口化?的技術(shù)來(lái)優(yōu)化性能,只需要渲染少部分區(qū)域的內(nèi)容,減少重新渲染組件和創(chuàng)建 dom 節(jié)點(diǎn)的時(shí)間。

vue-virtual-scroll-list 和 vue-virtual-scroller 都是解決這類(lèi)問(wèn)題的開(kāi)源項(xiàng)目。你也可以參考 Google 工程師的文章Complexities of an Infinite Scroller 來(lái)嘗試自己實(shí)現(xiàn)一個(gè)虛擬的滾動(dòng)列表來(lái)優(yōu)化性能,主要使用到的技術(shù)是 DOM 回收、墓碑元素和滾動(dòng)錨定。

Google 工程師繪制的無(wú)限列表設(shè)計(jì)


2.8 通過(guò)組件懶加載優(yōu)化超長(zhǎng)應(yīng)用內(nèi)容初始渲染性能

上面提到的無(wú)限列表的場(chǎng)景,比較適合列表內(nèi)元素非常相似的情況,不過(guò)有時(shí)候,你的 Vue 應(yīng)用的超長(zhǎng)列表內(nèi)的內(nèi)容往往不盡相同,例如在一個(gè)復(fù)雜的應(yīng)用的主界面中,整個(gè)主界面由非常多不同的模塊組成,而用戶看到的往往只有首屏一兩個(gè)模塊。在初始渲染的時(shí)候不可見(jiàn)區(qū)域的模塊也會(huì)執(zhí)行和渲染,帶來(lái)一些額外的性能開(kāi)銷(xiāo)。

使用組件懶加載在不可見(jiàn)時(shí)只需要渲染一個(gè)骨架屏,不需要真正渲染組件

你可以對(duì)組件直接進(jìn)行懶加載,對(duì)于不可見(jiàn)區(qū)域的組件內(nèi)容,直接不進(jìn)行加載和初始化,避免初始化渲染運(yùn)行時(shí)的開(kāi)銷(xiāo)。具體可以參考我們之前的專欄文章 性能優(yōu)化之組件懶加載: Vue Lazy Component 介紹,了解如何做到組件粒度的懶加載。


3. Vue 應(yīng)用加載性能優(yōu)化建議

3.1 利用服務(wù)端渲染(SSR)和預(yù)渲染(Prerender)來(lái)優(yōu)化加載性能

在一個(gè)單頁(yè)應(yīng)用中,往往只有一個(gè) html 文件,然后根據(jù)訪問(wèn)的 url 來(lái)匹配對(duì)應(yīng)的路由腳本,動(dòng)態(tài)地渲染頁(yè)面內(nèi)容。單頁(yè)應(yīng)用比較大的問(wèn)題是首屏可見(jiàn)時(shí)間過(guò)長(zhǎng)。

單頁(yè)面應(yīng)用顯示一個(gè)頁(yè)面會(huì)發(fā)送多次請(qǐng)求,第一次拿到 html 資源,然后通過(guò)請(qǐng)求再去拿數(shù)據(jù),再將數(shù)據(jù)渲染到頁(yè)面上。而且由于現(xiàn)在微服務(wù)架構(gòu)的存在,還有可能發(fā)出多次數(shù)據(jù)請(qǐng)求才能將網(wǎng)頁(yè)渲染出來(lái),每次數(shù)據(jù)請(qǐng)求都會(huì)產(chǎn)生 RTT(往返時(shí)延),會(huì)導(dǎo)致加載頁(yè)面的時(shí)間拖的很長(zhǎng)。

服務(wù)端渲染、預(yù)渲染和客戶端渲染的對(duì)比

這種情況下可以采用服務(wù)端渲染(SSR)和預(yù)渲染(Prerender)來(lái)提升加載性能,這兩種方案,用戶讀取到的直接就是網(wǎng)頁(yè)內(nèi)容,由于少了節(jié)省了很多 RTT(往返時(shí)延),同時(shí),還可以對(duì)一些資源內(nèi)聯(lián)在頁(yè)面,可以進(jìn)一步提升加載的性能。

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


3.2 通過(guò)組件懶加載優(yōu)化超長(zhǎng)應(yīng)用內(nèi)容加載性能

在上面提到的超長(zhǎng)應(yīng)用內(nèi)容的場(chǎng)景中,通過(guò)組件懶加載方案可以優(yōu)化初始渲染的運(yùn)行性能,其實(shí),這對(duì)于優(yōu)化應(yīng)用的加載性能也很有幫助。

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

使用組件懶加載之前的請(qǐng)求瀑布圖

使用組件懶加載之后的請(qǐng)求瀑布圖

使用組件懶加載方案對(duì)于超長(zhǎng)內(nèi)容的應(yīng)用初始化渲染很有幫助,可以減少大量必要的資源請(qǐng)求,縮短渲染關(guān)鍵路徑。

總結(jié)

本文總結(jié)了 Vue 應(yīng)用運(yùn)行時(shí)以及加載時(shí)的一些性能優(yōu)化措施,下面做一個(gè)回顧和概括:

1、Vue 應(yīng)用運(yùn)行時(shí)性能優(yōu)化措施

(1)引入生產(chǎn)環(huán)境的 Vue 文件

(2)使用單文件組件預(yù)編譯模板

(3)提取組件的 CSS 到單獨(dú)到文件

(4)利用Object.freeze()提升性能

(5)扁平化 Store 數(shù)據(jù)結(jié)構(gòu)

(6)合理使用持久化 Store 數(shù)據(jù)

(7)組件懶加載

2、Vue 應(yīng)用加載性能優(yōu)化措施

(1)服務(wù)端渲染 / 預(yù)渲染

(2)組件懶加載

文章總結(jié)的這些性能優(yōu)化手段當(dāng)然不能覆蓋所有的 Vue 應(yīng)用性能問(wèn)題,我們也會(huì)不斷總結(jié)和補(bǔ)充其他問(wèn)題及優(yōu)化措施,希望文章中提到這些實(shí)踐經(jīng)驗(yàn)?zāi)芙o你的 Vue 應(yīng)用性能優(yōu)化工作帶來(lái)小小的幫助。

作者:迅雷前端
鏈接:https://juejin.im/post/5b960fcae51d450e9d645c5f
來(lái)源:掘金

關(guān)注公眾號(hào)【grain先森】,回復(fù)關(guān)鍵詞 【18福利】,獲取為你準(zhǔn)備的年終福利,更多關(guān)鍵詞玩法期待你的探索~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容