初始加載資源過(guò)多
問(wèn)題起源于我們的一個(gè)頁(yè)面拔创,下面是這個(gè)頁(yè)面的截圖和初次請(qǐng)求的瀑布圖。
初始加載的時(shí)候陶冷,一共請(qǐng)求了155個(gè)資源儒溉,請(qǐng)求的瀑布圖就快要和頁(yè)面一樣長(zhǎng)了??
初始加載的資源過(guò)多導(dǎo)致在 domInteractive 之后,頁(yè)面花費(fèi)了大量時(shí)間加載子資源咨堤,導(dǎo)致頁(yè)面的 load 時(shí)長(zhǎng)被嚴(yán)重拖長(zhǎng)菇篡,達(dá)到了 5.6s 。
來(lái)看看這些子資源都是什么一喘,根據(jù)請(qǐng)求資源的類(lèi)型驱还,我們找到了最多的類(lèi)型是圖片,這是顯而易見(jiàn)的凸克,頁(yè)面上到處都是大圖片议蟆,其次是 js 文件,由第三方的業(yè)務(wù)插件和一些 JSONP 的接口組成触徐。
問(wèn)題分析
再回到最初的這個(gè)頁(yè)面咪鲜,結(jié)合上面的數(shù)據(jù)情況我們得出了這個(gè)頁(yè)面的問(wèn)題總結(jié)結(jié)論:
- 頁(yè)面由大量模塊組成
- 每個(gè)模塊部分由首頁(yè)自主維護(hù),部分由業(yè)務(wù)方通過(guò)插件維護(hù)
- 所有模塊是同時(shí)進(jìn)行加載
- 模塊中圖片內(nèi)容較多
- 每個(gè)模塊的依賴(lài)資源較多(包括js文件撞鹉、接口文件疟丙、css文件等)
解決思路
我們提出了下面兩個(gè)主要的解決思路:
組件化分治思想
為了方便后續(xù)的優(yōu)化颖侄,我們必須要求每個(gè)模塊之間降低耦合,將相關(guān)的邏輯(比如請(qǐng)求接口享郊、請(qǐng)求相關(guān)的依賴(lài)資源)都封裝在內(nèi)部览祖,在 Vue 里落實(shí)成組件的形式。
- 將各模塊拆分為組件粒度
- 將組件依賴(lài)的資源全部封裝在組件內(nèi)部進(jìn)行調(diào)用
加載優(yōu)先級(jí)
在完成了組件化的拆分炊琉,確保模塊之間不會(huì)互相影響和產(chǎn)生耦合之后展蒂,我們可以方面地調(diào)整加載策略。加載的策略是根據(jù)可見(jiàn)性來(lái)處理優(yōu)先級(jí)問(wèn)題苔咪。
- 優(yōu)先加載首屏可見(jiàn)模塊
- 其余不可見(jiàn)模塊懶加載锰悼,待可見(jiàn)或即將可見(jiàn)時(shí)加載
有了上面的解決思路,我們開(kāi)始思考具體的實(shí)現(xiàn):
如何解決判斷可見(jiàn)性問(wèn)題团赏?
從前我們都是通過(guò)監(jiān)聽(tīng)滾動(dòng)事件箕般、resize 事件來(lái)判斷模塊是否可見(jiàn),代碼不僅繁瑣舔清,而且一不小心沒(méi)有函數(shù)去抖就又可能導(dǎo)致嚴(yán)重的性能問(wèn)題丝里。
現(xiàn)在我們有了更好的選擇—— IntersectionObserver API ,IntersectionObserver 允許你配置一個(gè)回調(diào)函數(shù)体谒,每當(dāng) target 杯聚,元素和設(shè)備視口或者其他指定元素發(fā)生交集的時(shí)候該回調(diào)函數(shù)將會(huì)被執(zhí)行。這個(gè) API 的設(shè)計(jì)是異步的抒痒,而且保證你的回調(diào)執(zhí)行次數(shù)是非常有限的幌绍,而且回調(diào)是會(huì)在主線程空閑時(shí)才執(zhí)行,在性能方面表現(xiàn)更優(yōu)评汰,使用起來(lái)也更簡(jiǎn)單纷捞。
目前是現(xiàn)代瀏覽器支持,低版本瀏覽器可以通過(guò) polyfill 兼容被去。
如何盡可能懶的條件渲染主儡?
在解決了加載條件的判斷之后,我們需要解決加載條件為假的情況下不去渲染惨缆、加載條件為真的時(shí)候才渲染的問(wèn)題糜值,這里的答案非常簡(jiǎn)單:使用 Vue.js 提供的 v-if 指令,就可以做到真正的惰性渲染坯墨。
如果可見(jiàn)后進(jìn)行初始渲染寂汇,可見(jiàn)前如何顯示?
如果在判斷加載條件為假的時(shí)候捣染,什么都不渲染骄瓣,就會(huì)帶來(lái)一系列問(wèn)題:
- 用戶體驗(yàn)比較差,最開(kāi)始是白屏耍攘,然后突然又渲染出現(xiàn)內(nèi)容榕栏。
- 最致命的是我們判斷可見(jiàn)性是需要一個(gè)目標(biāo)來(lái)觀察的畔勤,如果什么不都渲染,我們就無(wú)從觀察扒磁。
這里引入一個(gè)骨架屏的概念庆揪,我們?yōu)檎鎸?shí)的組件做一個(gè)在尺寸、樣式上非常接近真實(shí)組件的組件妨托,叫做骨架屏缸榛。
骨架屏的作用有:
- 提升用戶感知體驗(yàn)
- 保證切換的一致性
- 提供可見(jiàn)性觀察的目標(biāo)對(duì)象
如何提升切換時(shí)的體驗(yàn)?
在真實(shí)組件開(kāi)始渲染的時(shí)候兰伤,需要一定的時(shí)間和空間内颗,時(shí)間指的是真實(shí)組件從創(chuàng)建到渲染的時(shí)間,包括請(qǐng)求接口医清、請(qǐng)求資源和渲染的時(shí)間起暮,空間指的是頁(yè)面布局中需要給真實(shí)組件留出剛好的位置,避免產(chǎn)生抖動(dòng)会烙。
這里我們可以使用 Vue.js 內(nèi)置的 transition 組件自定義骨架組件和真實(shí)組件的進(jìn)入和離開(kāi)效果,通過(guò)合理的布局和定位筒捺,減少切換時(shí)的抖動(dòng)柏腻,
通過(guò)設(shè)置過(guò)渡效果給真實(shí)組件留出一定的加載時(shí)間。
上面的問(wèn)題都有了答案之后系吭,我們很容易就可以實(shí)現(xiàn)一個(gè)通用的方案五嫂,來(lái)解決組件的懶加載問(wèn)題。
Vue組件懶加載方案介紹
項(xiàng)目Github地址: https://github.com/xunleif2e/vue-lazy-component
這個(gè)是我們基于上面的思考做的一個(gè)通用的解決方案肯尺,下面簡(jiǎn)單介紹一下特性沃缘、使用以及 API 方面的知識(shí),后面結(jié)合 5 個(gè)具體的 DEMO 來(lái)講解更高級(jí)的用法则吟。
特性
- 支持 組件可見(jiàn)或即將可見(jiàn)時(shí)懶加載
- 支持 組件延時(shí)加載
- 支持 加載組件前展示組件骨架槐臀,提高用戶體驗(yàn)
- 支持 懶加載組件分包異步加載
安裝和使用
npm i @xunlei/vue-lazy-component
- 方式1 利用插件方式全局注冊(cè)
- 方式2 局部注冊(cè)
- 方式3 獨(dú)立版本引入,自動(dòng)全局注冊(cè)
用法
Props
參數(shù) | 說(shuō)明 | 類(lèi)型 | 可選值 | 默認(rèn)值 |
---|---|---|---|---|
viewport | 組件所在的視口氓仲,如果組件是在頁(yè)面容器內(nèi)滾動(dòng)水慨,視口就是該容器 | HTMLElement | true |
null ,代表視窗 |
direction | 視口的滾動(dòng)方向, vertical 代表垂直方向敬扛,horizontal 代表水平方向 |
String | true | vertical |
threshold | 預(yù)加載閾值, css單位 | String | true | 0px |
tagName | 包裹組件的外層容器的標(biāo)簽名 | String | true | div |
timeout | 等待時(shí)間晰洒,如果指定了時(shí)間,不論可見(jiàn)與否啥箭,在指定時(shí)間之后自動(dòng)加載 | Number | true | - |
Events
事件名 | 說(shuō)明 | 事件參數(shù) |
---|---|---|
before-init | 模塊可見(jiàn)或延時(shí)截止導(dǎo)致準(zhǔn)備開(kāi)始加載懶加載模塊 | - |
init | 開(kāi)始加載懶加載模塊谍珊,此時(shí)骨架組件開(kāi)始消失 | - |
before-enter | 懶加載模塊開(kāi)始進(jìn)入 | el |
before-leave | 骨架組件開(kāi)始離開(kāi) | el |
after-leave | 骨架組件已經(jīng)離開(kāi) | el |
after-enter | 懶加載模快已經(jīng)進(jìn)入 | el |
after-init | 初始化完成 | - |
DEMO 1 超長(zhǎng)頁(yè)面懶加載
https://xunleif2e.github.io/vue-lazy-component/demo/dist/index.html#/large-page
<vue-lazy-component>
<st-series-sohu/>
<st-series-sohu-skeleton slot="skeleton"/>
</vue-lazy-component>
通過(guò)上面這種簡(jiǎn)單的使用方式就可以實(shí)現(xiàn)組件即將可見(jiàn)時(shí)自動(dòng)加載急侥。
DEMO 2 延時(shí)加載
https://xunleif2e.github.io/vue-lazy-component/demo/dist/index.html#/timeout
<vue-lazy-component :timeout="1000">
<st-series-sohu/>
<st-series-sohu-skeleton slot="skeleton"/>
</vue-lazy-component>
如果有時(shí)候僅僅是希望某些組件稍后渲染砌滞,而不一定要等到可見(jiàn)時(shí)炼七,可以通過(guò)這種方式。
比如我們業(yè)務(wù)中可能會(huì)有些運(yùn)營(yíng)性質(zhì)的掛件布持,就可以采取延時(shí)加載的方式豌拙。
DEMO 3 自定義過(guò)渡效果
https://xunleif2e.github.io/vue-lazy-component/demo/dist/index.html#/custom-transition
如果覺(jué)得 Vue Lazy Component 自帶的淡入淡出的過(guò)渡效果太丑,或者需要調(diào)整淡入淡出效果的時(shí)長(zhǎng)题暖,就可以通過(guò)自定義樣式來(lái)改變過(guò)渡效果按傅。這個(gè)例子演示了另外一種過(guò)渡效果,transition 的生命周期可以參考 Vue.js 的 transition 組件的文檔胧卤。
DEMO 4 webpack 分包
https://xunleif2e.github.io/vue-lazy-component/demo/dist/index.html#/large-page-chunks
DEMO1 演示了如何懶加載模塊唯绍,但其實(shí)只是推遲了模塊的渲染和模塊內(nèi)的資源的加載,如果我們需要更進(jìn)一步枝誊,連模塊本身的代碼也是懶加載况芒,就像 AMD 那樣異步按需加載,這個(gè)也是可以做到的叶撒。
這里可以利用Vue.js的異步組件绝骚,將每個(gè)真實(shí)組件都注冊(cè)成異步組件,在異步組件的工廠函數(shù)里使用 Webpack 的 AMD 版本的 require 祠够,就可以實(shí)現(xiàn)真實(shí)組件可以分成獨(dú)立的 bundle 加載压汪,脫離頁(yè)面 js 的bundle。
但是這里會(huì)有個(gè)問(wèn)題古瓤,就算模塊是可見(jiàn)時(shí)才渲染止剖,在打開(kāi)頁(yè)面的時(shí)候會(huì)發(fā)現(xiàn)模塊不可見(jiàn)之前它的 bundle 已經(jīng)加載了,這并沒(méi)有實(shí)現(xiàn)按需加載落君。
這個(gè)例子演示了一種做法穿香,Vue Lazy Component 可以在即將切換真實(shí)組件前通過(guò) Scoped Slots 傳遞一個(gè) loading 屬性給真實(shí)組件,真實(shí)組件只要是根據(jù)這個(gè) loading 來(lái)?xiàng)l件渲染就可以避免非按需加載绎速,這個(gè)和 Vue.js 對(duì)組件的解析機(jī)制有關(guān)皮获,例子里有相應(yīng)的的代碼,有興趣的同學(xué)可以深入研究下朝氓。
DEMO 5 特定視口內(nèi)懶加載
https://xunleif2e.github.io/vue-lazy-component/demo/dist/index.html#/specific-viewport
在某些場(chǎng)景下魔市,我們要解決滾動(dòng)容器內(nèi)的組件懶加載,這個(gè)時(shí)候可見(jiàn)性是相對(duì)與這個(gè)視口來(lái)的赵哲,這個(gè)例子演示了如何指定聊天窗口作為觀察的視口待德。
這里吐槽下Vue.js的 $parent 、$refs 的設(shè)計(jì)枫夺,它們都不是響應(yīng)式的将宪,如果需要?jiǎng)討B(tài)獲取這些組件引用上的 $el ,必須要等到 mounted 事件發(fā)生之后,所以例子的代碼稍微有一點(diǎn)繁瑣较坛。
應(yīng)用效果
首先 Vue Lazy Component 的設(shè)計(jì)雖然是說(shuō)組件級(jí)的印蔗,其實(shí)它的粒度可大可小,大的比如頁(yè)面不同的區(qū)域丑勤,小的就像 DEMO5 里的只是一個(gè)用戶頭像华嘹,所以適用性非常強(qiáng),只要有懶加載需求的場(chǎng)景基本都可以采用法竞。
另外耙厚,在終端方面,不僅可以兼容PC端的項(xiàng)目岔霸,在移動(dòng)端也是可以使用的薛躬,當(dāng)然,需要解決 IntersectionObserver API 的兼容性問(wèn)題呆细,在項(xiàng)目 Readme 里提到了 w3c 的 polyfill 的地址型宝。
應(yīng)用業(yè)務(wù)
我們目前應(yīng)用在迅雷的兩個(gè)項(xiàng)目中,一個(gè)是 PC 迅雷的首頁(yè)項(xiàng)目絮爷,一個(gè)是 PC 迅雷的組隊(duì)加速項(xiàng)目趴酣,后期預(yù)計(jì)會(huì)推廣到更多的業(yè)務(wù)中去。
優(yōu)化后請(qǐng)求瀑布圖
我們?cè)賮?lái)看看開(kāi)始那個(gè)頁(yè)面的情況略水,在使用了 組件懶加載技術(shù)后价卤,請(qǐng)求數(shù)變成了只有 31 個(gè),瀑布圖變得比較短了渊涝。
數(shù)據(jù)對(duì)比
我們把前后的數(shù)據(jù)進(jìn)行一個(gè)對(duì)比:
- 請(qǐng)求數(shù)變成之前的 1 / 5,優(yōu)化效果比較明顯
- 請(qǐng)求大小相比之前降低不太明顯
- load 時(shí)長(zhǎng)也同樣不太明顯
分析主要是有較多圖片未按照使用尺寸裁剪和壓縮床嫌,導(dǎo)致請(qǐng)求大小較大跨释,同時(shí)造成了 load 時(shí)長(zhǎng)的拖長(zhǎng)。
后續(xù)性能優(yōu)化方向
后續(xù)我們會(huì)繼續(xù)來(lái)優(yōu)化這個(gè)頁(yè)面厌处,主要的方向有兩個(gè):
圖片尺寸適配和壓縮
通過(guò)圖片的裁剪和壓縮鳖谈,解決請(qǐng)求資源大小較大子資源加載時(shí)間較長(zhǎng)導(dǎo)致 load 時(shí)間拖長(zhǎng)的問(wèn)題
預(yù)渲染
采用預(yù)渲染插件將頁(yè)面的主要 css 、js 進(jìn)行內(nèi)聯(lián)阔涉,將骨架架屏通過(guò)預(yù)渲染生成出來(lái)缆娃,這樣可以避免 SPA 首屏可見(jiàn)關(guān)鍵路徑較長(zhǎng)的問(wèn)題,在頁(yè)面解析完 dom 樹(shù)以后即可保證首屏可見(jiàn)瑰排。
懶加載方案 ROADMAP
Vue Lazy Component 懶加載方案還有些地方做得還不夠好贯要,計(jì)劃在后期的幾個(gè)小版本里支持以下的特性:
- SSR 支持 v1.1.0
- UI單元測(cè)試 v1.2.0
- 減少性能開(kāi)銷(xiāo) v1.3.0
- 重繪
- FPS
后記
這篇文章分享了從遇到業(yè)務(wù)實(shí)際性能問(wèn)題,到分析椭住、解決并梳理出通用的解決方案的過(guò)程崇渗,重點(diǎn)其實(shí)不是最終的實(shí)現(xiàn)代碼實(shí)現(xiàn),而是解決問(wèn)題的角度和過(guò)程。
最后歡迎大家通過(guò)提交 issue 或者 PR 的方式參與貢獻(xiàn)宅广,項(xiàng)目 Github 地址: https://github.com/xunleif2e/vue-lazy-component 葫掉。