性能優化之組件懶加載: Vue Lazy Component 介紹

初始加載資源過多

問題起源于我們的一個頁面,下面是這個頁面的截圖和初次請求的瀑布圖。


初始加載了155個請求

初始加載的時候,一共請求了155個資源,請求的瀑布圖就快要和頁面一樣長了??

請求概況

初始加載的資源過多導致在 domInteractive 之后,頁面花費了大量時間加載子資源,導致頁面的 load 時長被嚴重拖長,達到了 5.6s 。

PerformanceNavigationTiming 信息

來看看這些子資源都是什么,根據請求資源的類型,我們找到了最多的類型是圖片,這是顯而易見的,頁面上到處都是大圖片,其次是 js 文件,由第三方的業務插件和一些 JSONP 的接口組成。

請求資源類型分布

問題分析

頁面結構拆分

再回到最初的這個頁面,結合上面的數據情況我們得出了這個頁面的問題總結結論:

  • 頁面由大量模塊組成
  • 每個模塊部分由首頁自主維護,部分由業務方通過插件維護
  • 所有模塊是同時進行加載
  • 模塊中圖片內容較多
  • 每個模塊的依賴資源較多(包括js文件、接口文件、css文件等)

解決思路

我們提出了下面兩個主要的解決思路:

組件化分治思想

為了方便后續的優化,我們必須要求每個模塊之間降低耦合,將相關的邏輯(比如請求接口、請求相關的依賴資源)都封裝在內部,在 Vue 里落實成組件的形式。

  • 將各模塊拆分為組件粒度
  • 將組件依賴的資源全部封裝在組件內部進行調用

加載優先級

在完成了組件化的拆分,確保模塊之間不會互相影響和產生耦合之后,我們可以方面地調整加載策略。加載的策略是根據可見性來處理優先級問題。

  • 優先加載首屏可見模塊
  • 其余不可見模塊懶加載,待可見或即將可見時加載

有了上面的解決思路,我們開始思考具體的實現:

如何解決判斷可見性問題?

從前我們都是通過監聽滾動事件、resize 事件來判斷模塊是否可見,代碼不僅繁瑣,而且一不小心沒有函數去抖就又可能導致嚴重的性能問題。

現在我們有了更好的選擇—— IntersectionObserver API ,IntersectionObserver 允許你配置一個回調函數,每當 target ,元素和設備視口或者其他指定元素發生交集的時候該回調函數將會被執行。這個 API 的設計是異步的,而且保證你的回調執行次數是非常有限的,而且回調是會在主線程空閑時才執行,在性能方面表現更優,使用起來也更簡單。

http://caniuse.com/#search=IntersectionObserver

目前是現代瀏覽器支持,低版本瀏覽器可以通過 polyfill 兼容。

如何盡可能懶的條件渲染?

在解決了加載條件的判斷之后,我們需要解決加載條件為假的情況下不去渲染、加載條件為真的時候才渲染的問題,這里的答案非常簡單:使用 Vue.js 提供的 v-if 指令,就可以做到真正的惰性渲染。

如果可見后進行初始渲染,可見前如何顯示?

如果在判斷加載條件為假的時候,什么都不渲染,就會帶來一系列問題:

  • 用戶體驗比較差,最開始是白屏,然后突然又渲染出現內容。
  • 最致命的是我們判斷可見性是需要一個目標來觀察的,如果什么不都渲染,我們就無從觀察。

這里引入一個骨架屏的概念,我們為真實的組件做一個在尺寸、樣式上非常接近真實組件的組件,叫做骨架屏。

骨架屏

骨架屏的作用有:

  • 提升用戶感知體驗
  • 保證切換的一致性
  • 提供可見性觀察的目標對象

如何提升切換時的體驗?

在真實組件開始渲染的時候,需要一定的時間和空間,時間指的是真實組件從創建到渲染的時間,包括請求接口、請求資源和渲染的時間,空間指的是頁面布局中需要給真實組件留出剛好的位置,避免產生抖動。

這里我們可以使用 Vue.js 內置的 transition 組件自定義骨架組件和真實組件的進入和離開效果,通過合理的布局和定位,減少切換時的抖動,
通過設置過渡效果給真實組件留出一定的加載時間。

上面的問題都有了答案之后,我們很容易就可以實現一個通用的方案,來解決組件的懶加載問題。

Vue組件懶加載方案介紹

項目Github地址: https://github.com/xunleif2e/vue-lazy-component

這個是我們基于上面的思考做的一個通用的解決方案,下面簡單介紹一下特性、使用以及 API 方面的知識,后面結合 5 個具體的 DEMO 來講解更高級的用法。

特性

  • 支持 組件可見或即將可見時懶加載
  • 支持 組件延時加載
  • 支持 加載組件前展示組件骨架,提高用戶體驗
  • 支持 懶加載組件分包異步加載

安裝和使用

npm i @xunlei/vue-lazy-component
  • 方式1 利用插件方式全局注冊
  • 方式2 局部注冊
  • 方式3 獨立版本引入,自動全局注冊

用法

使用方式

Props

參數 說明 類型 可選值 默認值
viewport 組件所在的視口,如果組件是在頁面容器內滾動,視口就是該容器 HTMLElement true null,代表視窗
direction 視口的滾動方向, vertical代表垂直方向,horizontal代表水平方向 String true vertical
threshold 預加載閾值, css單位 String true 0px
tagName 包裹組件的外層容器的標簽名 String true div
timeout 等待時間,如果指定了時間,不論可見與否,在指定時間之后自動加載 Number true -

Events

事件名 說明 事件參數
before-init 模塊可見或延時截止導致準備開始加載懶加載模塊 -
init 開始加載懶加載模塊,此時骨架組件開始消失 -
before-enter 懶加載模塊開始進入 el
before-leave 骨架組件開始離開 el
after-leave 骨架組件已經離開 el
after-enter 懶加載模快已經進入 el
after-init 初始化完成 -

DEMO 1 超長頁面懶加載

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>

通過上面這種簡單的使用方式就可以實現組件即將可見時自動加載。

DEMO 2 延時加載

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>

如果有時候僅僅是希望某些組件稍后渲染,而不一定要等到可見時,可以通過這種方式。

比如我們業務中可能會有些運營性質的掛件,就可以采取延時加載的方式。

DEMO 3 自定義過渡效果

https://xunleif2e.github.io/vue-lazy-component/demo/dist/index.html#/custom-transition

自定義過渡效果

如果覺得 Vue Lazy Component 自帶的淡入淡出的過渡效果太丑,或者需要調整淡入淡出效果的時長,就可以通過自定義樣式來改變過渡效果。這個例子演示了另外一種過渡效果,transition 的生命周期可以參考 Vue.js 的 transition 組件的文檔。

DEMO 4 webpack 分包

https://xunleif2e.github.io/vue-lazy-component/demo/dist/index.html#/large-page-chunks

懶加載組件異步加載

DEMO1 演示了如何懶加載模塊,但其實只是推遲了模塊的渲染和模塊內的資源的加載,如果我們需要更進一步,連模塊本身的代碼也是懶加載,就像 AMD 那樣異步按需加載,這個也是可以做到的。

這里可以利用Vue.js的異步組件,將每個真實組件都注冊成異步組件,在異步組件的工廠函數里使用 Webpack 的 AMD 版本的 require ,就可以實現真實組件可以分成獨立的 bundle 加載,脫離頁面 js 的bundle。

但是這里會有個問題,就算模塊是可見時才渲染,在打開頁面的時候會發現模塊不可見之前它的 bundle 已經加載了,這并沒有實現按需加載。

這個例子演示了一種做法,Vue Lazy Component 可以在即將切換真實組件前通過 Scoped Slots 傳遞一個 loading 屬性給真實組件,真實組件只要是根據這個 loading 來條件渲染就可以避免非按需加載,這個和 Vue.js 對組件的解析機制有關,例子里有相應的的代碼,有興趣的同學可以深入研究下。

DEMO 5 特定視口內懶加載

https://xunleif2e.github.io/vue-lazy-component/demo/dist/index.html#/specific-viewport

IM場景

在某些場景下,我們要解決滾動容器內的組件懶加載,這個時候可見性是相對與這個視口來的,這個例子演示了如何指定聊天窗口作為觀察的視口。

這里吐槽下Vue.js的 $parent 、$refs 的設計,它們都不是響應式的,如果需要動態獲取這些組件引用上的 $el ,必須要等到 mounted 事件發生之后,所以例子的代碼稍微有一點繁瑣。

應用效果

首先 Vue Lazy Component 的設計雖然是說組件級的,其實它的粒度可大可小,大的比如頁面不同的區域,小的就像 DEMO5 里的只是一個用戶頭像,所以適用性非常強,只要有懶加載需求的場景基本都可以采用。

另外,在終端方面,不僅可以兼容PC端的項目,在移動端也是可以使用的,當然,需要解決 IntersectionObserver API 的兼容性問題,在項目 Readme 里提到了 w3c 的 polyfill 的地址。

應用業務

我們目前應用在迅雷的兩個項目中,一個是 PC 迅雷的首頁項目,一個是 PC 迅雷的組隊加速項目,后期預計會推廣到更多的業務中去。

迅雷前端項目

優化后請求瀑布圖

我們再來看看開始那個頁面的情況,在使用了 組件懶加載技術后,請求數變成了只有 31 個,瀑布圖變得比較短了。

請求數變為31個

數據對比

我們把前后的數據進行一個對比:

  • 請求數變成之前的 1 / 5,優化效果比較明顯
  • 請求大小相比之前降低不太明顯
  • load 時長也同樣不太明顯

分析主要是有較多圖片未按照使用尺寸裁剪和壓縮,導致請求大小較大,同時造成了 load 時長的拖長。

后續性能優化方向

后續我們會繼續來優化這個頁面,主要的方向有兩個:

圖片尺寸適配和壓縮

通過圖片的裁剪和壓縮,解決請求資源大小較大子資源加載時間較長導致 load 時間拖長的問題

預渲染

采用預渲染插件將頁面的主要 css 、js 進行內聯,將骨架架屏通過預渲染生成出來,這樣可以避免 SPA 首屏可見關鍵路徑較長的問題,在頁面解析完 dom 樹以后即可保證首屏可見。

懶加載方案 ROADMAP

Vue Lazy Component 懶加載方案還有些地方做得還不夠好,計劃在后期的幾個小版本里支持以下的特性:

  • SSR 支持 v1.1.0
  • UI單元測試 v1.2.0
  • 減少性能開銷 v1.3.0
    • 重繪
    • FPS

后記

這篇文章分享了從遇到業務實際性能問題,到分析、解決并梳理出通用的解決方案的過程,重點其實不是最終的實現代碼實現,而是解決問題的角度和過程。

最后歡迎大家通過提交 issue 或者 PR 的方式參與貢獻,項目 Github 地址: https://github.com/xunleif2e/vue-lazy-component

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容