小程序頁面加載性能優化

6月份有幸被前端早讀課邀請到廈門大前端技術沙龍,做了小程序頁面加載性能優化的分享。從現場反響來看,你們對性能優化的關注度都很高,仍是很開心的。html

本文是分享的文字版,總結了咱們上半年對榛果小程序性能優化的研究和一些實踐,但願各位讀者可以對小程序的性能優化有個系統的瞭解。前端

小程序出現的問題及優化方案

你們在開發和使用小程序的時候,是否有遇到這兩個問題?web

問題一,頁面的加載時間過長:當頁面在跳轉時,有較長的白屏時間或「加載中」的狀態;算法

問題二,頁面失去響應:頁面在滑動過程當中出現卡頓,或按鈕點擊後失去響應;小程序

本文將圍繞這兩個問題的解決思路進行展開。segmentfault

問題一 加載時間過長

新頁面在加載過程當中除了要建立頁面實例初始化等工做,更多時間還花在頁面數據的請求上。因此針對數據請求,咱們提出了三種優化思路:後端

  1. 請求前置:提早請求數據,縮短頁面加載時間;微信小程序

  2. 首屏直出:請求前,利用已有數據,跳過「頁面加載」的過程;promise

  3. 數據緩存:請求後,緩存接口請求的數據;緩存


優化思路一 請求前置

咱們通常會在新頁面加載完成的onLoad事件中請求數據,那咱們可不能夠考慮提早請求的時機?在回答這個問題前,咱們須要先了解一下小程序的運行機制和生命週期。

小程序MINA框架


小程序總體框架分爲三個層,視圖層、邏輯層和系統層。視圖層包含了小程序各個頁面的WXML、WXS和WXSS。邏輯層則運行在JavaScript執行引擎中,包含處理數據邏輯和封裝客戶端提供的API。 系統層則提供微信客戶端原生的能力。除了微信原生功能支持、網絡請求等,還提供視圖層和邏輯層之間數據傳輸與事件通知的功能。

邏輯層將數據變化通知給視圖層,觸發頁面更新;視圖層把觸發的事件通知給邏輯層,進行業務處理。能夠看出,視圖層的變化其實是由數據驅動的,小程序將邏輯層的數據反應成視圖,同時將視圖層的事件發送給邏輯層,這中間須要系統層作中轉傳輸。

根據小程序框架,咱們瞭解視圖渲染與邏輯執行是並行獨立的線程,所以咱們能夠一邊作頁面過渡,一邊向後端請求數據,這樣並行作請求與渲染。那麼,問題又來了,咱們應該把數據請求觸發點應該提早到那裏呢?能夠小程序頁面加載的生命週期中找到一些思路。

生命週期


頁面加載的生命週期通過:

  • 視圖層初始化,同時邏輯層頁面實例建立後,觸發onLoad和onShow事件,等待視圖層初始化完成後的通知,再發送頁面的初始數據;

  • 視圖層接受初始數據後,完成首次渲染,通知邏輯層,邏輯層觸發onReady事件,至此頁面首次渲染完成,已準備穩當可與視圖層進行交互;

  • 以後,邏輯層再調用setData,觸發視圖層的再次渲染,視圖層再回調通知邏輯層,完成一次數據渲染操做;

通常咱們都會將請求放在onLoad事件,等請求返回數據時,再調用setData渲染頁面。若是前置請求,能夠考慮提早到頁面打開以前,這樣可以更早接受到請求返回的數據。

將本來onLoad事件開始的請求前置到頁面跳轉,縮短的時間是跳轉到onLoad事件之間的時間,這段時間大約100~300ms(簡單實驗了一下,iphoneX需花費150+ms,小米MIX2需花費200+ms)。


請求前置的實現

可能有同窗會有疑問:在咱們跳轉的時候,下一個頁面實例都尚未被建立,又該如何請求數據?難道要把下個頁面的請求代碼耦合進這個頁面裏嗎?這樣代碼業務邏輯耦合度過高,不利於維護。

其實,微信小程序有獨特的頁面機制能夠在頁面沒有建立實例前拿到頁面對象的各個方法。小程序啓動時,會調用Page函數註冊全部的頁面,運行時能夠取到全部頁面原型,頁面被訪問時建立的是基於這個原型所建立的實例。因此,咱們能夠包裝註冊頁面的Page函數,收集頁面原型的引用,從而獲得頁面中的方法

爲了實現請求前置,咱們設計了一個FastOpen單例對象,其主要結構以下圖:


FastOpen單例對象能夠用來處理全部請求前置跳轉的邏輯。其中包括了:

  • pageList:其元素記錄了每個頁面的名稱、原型引用和獲取前置請求數據的方法;

  • register:用於註冊頁面。Page函數能夠被包裝成一個新的CommonPage函數,統一作每一個頁面都須要作的額外工做,例如埋點、性能數據收集以及將頁面註冊進FastOpen。咱們將在CommonPage執行時,調用該方法,將頁面名稱、引用等push到pageList中,業務頁面中實現initQuery,包含能夠前置的請求;

  • initPage:調用目標頁的initQuery,發送那些被前置的請求。假設一個場景,頁面A跳轉到頁面B,當頁面A跳轉的時候能夠經過FastOpen的initPage調用頁面B的initQuery,並將其包裝成promise,賦值給頁面B的initDataPromise;

  • getDataPromise:獲取頁面前置請求的數據。頁面A跳轉到頁面B,當頁面B加載完成後,就能夠調用該方法,獲取頁面B的前置請求數據;


優化思路二 首屏直出

在發送請求以前,咱們能夠考慮的優化方式是首屏直出優化:利用前置頁已有的數據,先將部分首屏數據展現給用戶,跳過「加載中」的過程,作到首屏頁面直出。

從業務中能夠發現,前置頁中有能夠用來首屏渲染的數據,好比咱們榛果民宿業務,前置頁(各類房源列表)就已經有房源詳情頁中的部分數據,如首圖、標題,房型,價格,可住人數等等。所以,咱們就能夠利用前置頁的數據來渲染一個簡單的詳情頁,至少包含首屏的大部分數據。


若是沒有可用的數據也沒關係,咱們能夠考慮骨架屏方案,及時響應用戶操做。

咱們再回到生命週期。優化前,頁面出現首屏內容應該是在請求回來再次渲染完成以後。優化後,利用已有數據作首屏直接渲染,是能夠在onLoad的時候就進行setData。以下圖所示:


首屏直出的實現


爲了實現目標頁也能夠取到前置頁的部分數據,咱們須要維護一個全局變量Map,key是每一個頁面實例惟一的id,value是能夠給目標頁首屏渲染的初始化數據。

整個流程是:

  • 前置頁準備目標頁首屏渲染的數據,存儲到全局變量Map中,同時生成目標頁的pageId;

  • pageId傳回前置頁,作跳轉帶上pageId;

  • 目標頁面利用pageId獲取數據,能夠直接渲染首屏內容,無需明顯的加載過程;


優化思路三 數據緩存

發送的請求返回時,咱們能夠作的優化是緩存它,讓下一次相同的請求能夠更快速響應。緩存用到的是小程序的本地緩存,有如下幾個特色:

  • 緩存總大小隻有10MB;

  • 緩存超過10MB會寫失敗;

  • 緩存對於線上版、體驗版、開發版都是共用的;

  • 但對於不一樣小程序和用戶,出於數據隱私的緣由,它們是隔離的;

因爲緩存大小有限,咱們須要緩存置換算法,使用的LRU(最近最少使用算法)。主要思路是維護一個隊列,當有新元素進隊且隊列滿了的狀況下,淘汰隊尾元素;而當隊列中的元素被使用時,則將其移動到對首。

數據緩存設計


咱們設計一個緩存索引來幫助咱們執行LRU算法。索引的key是接口path和字符串化的參數,value包含過時時間,用這個時間來排序淘汰最先過時的數據元素;以及數據大小,爲了計算須要淘汰多少個元素。

須要緩存的數據則用與索引相同方法計算出key,將數據直接存在storage裏。每次取數時更新索引的過時時間後,再取緩存數據。


問題二 頁面失去響應

接下來,咱們討論問題二「頁面失去響應」。要想解決這個問題,能夠從渲染優化入手。

小程序渲染機制

渲染優化前,先再回顧一下小程序的渲染機制:


視圖與邏輯屬於不一樣線程,沒法直接通訊,數據傳輸主要經過系統層進行。邏輯層調用setData將數據變化通知給視圖層,通過系統層觸發頁面更新。

每次setData都有跨進程通訊的開銷,而且視圖層反饋計算與更新也是須要必定時間的,在這段時間用戶的操做也會受影響。

優化思路四 渲染優化

渲染優化能夠考慮五點:

1. 合併setData數據

將屢次setData操做合併成一次,儘可能減小邏輯層和視圖層的通訊開銷;

2. 移除非視圖層數據

控制頁面data對象的大小,將非視圖層數據移出page的data對象,非視圖層的數據即未在wxml中使用的數據。

例如:


上面例子中,myText是視圖層的數據,須要寫在data中,而_myText則是方法間共享的變量,能夠像右圖同樣移出data對象,在onLoad中初始化,更新時直接「=」賦值。

3. 精簡setData數據

儘可能精簡setData參數,利用數據路徑形式的key只提交改變部分。

這實際上是setData提供的語法糖,例以下圖代碼中的場景:


data裏有個userInfo對象,有一部分如姓名是不會變更的,而另外一個字段isStudent可能須要校驗後才能知道是否爲學生。在校驗後若是須要更改isStudent值,就通常作法須要setData方法賦值整個userInfo對象。而有了這個語法糖,咱們能夠參考右邊的代碼,直接用數據路徑的形式,只提交更改的那部分。

4.中止後臺頁面setData

對於切換到後臺的頁面,應該中止對setData進行操做。

由於全部webview共享同一個JS運行環境,後臺頁面的setData會影響當前頁面的渲染。最多見的場景就是倒計時秒殺組件,咱們能夠在頁面onHide事件把計時器停掉,等onShow的時候再開啓繼續計時。



小程序性能衡量指標

對於性能優化效果要如何衡量?咱們約定了兩個指標:

  1. 首次渲染時間:頁面首次渲染完成,表示頁面已經加載能夠和視圖層交互;

  2. 首屏加載時間:首屏部分的內容已經渲染完成,用戶能夠看到首屏;

從頁面加載生命週期來看,他們分別對應於:


首次渲染時間,能夠用onReady事件,它就表示頁面首次渲染完成能夠與視圖層交互,故而爲 onReady - onLoad。


首屏加載時間,其開始時間一樣是onLoad,至於結束時間,setData方法有回調,是在頁面渲染完畢後執行的回調函數。咱們用首屏數據setData的這個回調做爲首屏加載時間的結束點。故而,首屏加載時間是 首屏setData - onLoad。

效果分析


從體驗上來看,相比優化前,優化後沒有加載中的過程,內容直接展現,體驗流暢。

從數據上看,咱們取AB測試一天的時間進行分析,TP90的數據顯示:優化前是3.1s,優化後約1.4s,時間縮短了1.7s,縮短了55%,優化效果仍是很明顯的。


總結

尋找性能優化的思路,咱們能夠從這三方面入手:

  1. 體驗:給用戶提供良好流暢的體驗,例如首屏直出,提早展現頁面內容及時響應用戶操做;

  2. 原理:從小程序的運行機制和生命週期出發,利用獨特的頁面機制,提早請求數據縮短頁面時間;理解渲染機制,避免性能較差的編碼方法;

  3. 數據:從接口、數據模塊、頁面三個層面考慮數據緩存,儘可能提升命中率;


參考文檔

相關文章
相關標籤/搜索