淺談小程式效能優化

玉米同學發表於2019-06-29

1 優化方向

  • 啟動載入效能

  • 渲染效能


2 啟動載入效能

2.1 啟動載入原理

小程式啟動主要分為邏輯層的啟動和檢視層的啟動。邏輯層執行js程式碼邏輯,檢視層以 webview 為載體,完成頁面內容的渲染和更新。

小程式啟動前,客戶端會對小程式的基礎環境進行預載入,提升小程式載入的速度。在使用者開啟小程式的時,會首先進行程式碼包的下載,下載完成後分別在邏輯層和檢視層注入執行開發者的業務程式碼,最終將執行結果聚合渲染出首屏內容。

å°ç¨åºè¿è¡

即小程式啟動過程三個階段:資源準備(程式碼包下載);業務程式碼的注入以及落地頁首次渲染;落地頁請求時的loading態(當落地頁不要請求任何資料時就沒有這個過程)。

舉例:

淺談小程式效能優化

圖1:下載小程式程式碼包。小程式主要在進行資源準備,主要是程式碼包的下載和校驗工作。

圖2:載入小程式程式碼包。小程式在進行業務程式碼的注入和執行,等待首次渲染完成。

圖3:初始化小程式首頁。當小程式完成第一次渲染後,對於很多小程式並不意味著頁面內容完全出現,而需要與伺服器進行一次的通訊,獲取資料來進行渲染。圖3就是這類場景下等待請求返回的過程。


2.2 控制程式碼包大小

小程式載入過程中,程式碼包的大小最直接影響整個小程式載入啟動效能。

所以,提升小程式的啟動載入效能,最直接有效的就是減少程式碼包的大小。

減小程式碼包的大小的方式:

  • 在開發者工具中開啟“程式碼壓縮”的選項。
  • 即時清理廢棄的程式碼,尤其是比較大的第三方庫,以及一些不使用的圖片等資原始檔。
  • 減少本地圖片等資原始檔,必要時使用網路圖片程式碼。


2.3 分包載入機制

根據業務場景,將使用者訪問率高的頁面放在主包裡,將訪問率低的頁面放入子包裡,按需載入;

淺談小程式效能優化

在小程式啟動時,預設會下載主包並啟動主包內頁面,當使用者進入分包內某個頁面時,客戶端會把對應分包下載下來,下載完成後再進行展示。


2.4 獨立分包

獨立分包是小程式中一種特殊型別的分包,可以獨立於主包和其他分包執行。從獨立分包中頁面進入小程式時,不需要下載主包。當使用者進入普通分包或主包內頁面時,主包才會被下載。

淺談小程式效能優化

可以按需將某些具有一定功能獨立性的頁面配置到獨立分包中。當小程式從普通的分包頁面啟動時,需要首先下載主包;而獨立分包不依賴主包即可執行,可以很大程度上提升分包頁面的啟動速度。


2.5 分包預下載

淺談小程式效能優化

開發者可以通過配置,在進入小程式某個頁面時,由框架自動預下載可能需要的分包,提升進入後續分包頁面時的啟動速度。對於獨立分包,也可以預下載主包。


2.6 首屏優化載入建議

  • 提前請求

資料請求並不依賴頁面結構完整,可以在頁面載入時或程式碼注入時即 在頁面 onload 就發起,而不需要等待頁面渲染完成。使用者等待請求返回的時間就會進一步縮短。

  • 利用快取

利用storage API 對請求結果請進快取,二次啟動時,直接用快取資料完成渲染,然後再在後臺進行據更新,保證使用者第一時間看到頁面內容,同時,即使在無網環境下,使用者也可以使用小程式的部分功能。

  • 避免白屏

請求過程中,在頁面中先展示一個基礎的骨架和結合已有的資料進行展示,可以讓使用者對頁面內容有一個心理預期,減少在等待的時候離開的可能。

  • 及時反饋

對於一些耗時的操作,在使用者等待的過程中,即使給予互動操作的反饋,避免使用者以為小程式無響應。


3 渲染效能優化

3.1 小程式渲染原理

小程式是基於 雙執行緒 模型的。

在這種架構中,小程式的渲染層使用 WebView 作為渲染載體,而邏輯層則由獨立的 JsCore 執行緒執行 JS 指令碼,雙方並不具備資料直接共享的通道,因此渲染層和邏輯層的通訊要由 Native 的 JSBrigde 做中轉。

淺談小程式效能優化

小程式更新檢視資料的通訊流程:

每當小程式檢視資料需要更新時,邏輯層會呼叫小程式宿主環境提供的 setData 方法將資料從邏輯層傳遞到檢視層,經過一系列渲染步驟之後完成UI檢視更新。完整的通訊流程如下:

  • 小程式邏輯層呼叫宿主環境的 setData 方法。

  • 邏輯層執行 JSON.stringify 將待傳輸資料轉換成字串並拼接到特定的JS指令碼,並通過evaluateJavascript 執行指令碼將資料傳輸到渲染層。

  • 渲染層接收到後, WebView JS 執行緒會對指令碼進行編譯,得到待更新資料後進入渲染佇列等待 WebView 執行緒空閒時進行頁面渲染。

  • WebView 執行緒開始執行渲染時,待更新資料會合併到檢視層保留的原始 data 資料,並將新資料套用在WXML片段中得到新的虛擬節點樹。經過新虛擬節點樹與當前節點樹的 diff 對比,將差異部分更新到UI檢視。同時,將新的節點樹替換舊節點樹,用於下一次重渲染。

綜上,邏輯層使用 setData 向檢視層傳輸資料,由檢視層進行頁面更新。從架構上,邏輯層和檢視層無法直接共享資料的,資料傳輸是一次跨程式的通訊,會有一定的通訊開銷,這一開銷與傳輸的資料量正相關。


3.2 正確使用 setData

  • 避免在 data 中放置與渲染無關的資料,只在 data 中放置與頁面渲染相關的資料。
  • 避免使用 setData 一次性傳輸大量資料,只對發生變化的資料進行 setData。當資料量達到 1MB 時,耗時會增加到數百毫秒,在一些低端機型上可能需要1s甚至更久。

比如,對於長列表,利用 setData 進行列表區域性重新整理。

// 後臺獲取列表資料
const list = requestSync(); 
// 更新整個列表
this.setData({ list });複製程式碼

實際上,只有個別欄位需要更新時,我們可以這麼寫來避免整個 list 列表更新:

// 後臺獲取列表資料
const list = requestSync(); 
// 區域性更新列表
this.setData({   
 'list[0].src': list[0].src
});複製程式碼

又或者是:

// 1.通過一個二維陣列來儲存資料
let feedList = [[array]];

// 2.維護一個頁面變數值,載入完一次資料page++
let page = 1

// 3.頁面每次滾動到底部,通過資料路徑更新資料
onReachBottom:()=>{    
    fetchNewData().then((newVal)=>{        
        this.setData({            
            ['feedList[' + (page - 1) + ']']: newVal,        
        })    
    }
}

// 4.最終我們的資料是[[array1],[array2]]這樣的格式,然後通過wx:for遍歷渲染資料複製程式碼
  • 不要在短時間內連續的頻繁呼叫 setData,對連續的 setData 儘可能的進行合併。頻繁呼叫會導致 webview 在一段時間內一直在進行資料資料處理和頁面更新,無法即使響應使用者操作,造成操作卡頓,也無法及時將使用者的操作事件反饋給邏輯層,造成互動延遲。
  • 切勿在後臺頁面進行setData

小程式的每個頁面雖然是在獨立的 webview 中執行的,但是 webview 的 js 引擎是共享的,如果一個頁面出現 setData 的誤用,會搶佔資源,影響其他頁面的執行。

舉例:頁面中有一個倒數計時,比如電商的秒殺活動,當秒殺頁面進入後臺,如果沒有定製定時器,那這時後臺頁面還是會不斷進行更新,搶佔了當前資源。


3.3 事件的正確使用

檢視層將事件反饋給邏輯層時,同樣需要一個通訊過程,通訊的方向是從檢視層到邏輯層。因為這個通訊過程是非同步的,會產生一定的延遲,延遲時間同樣與傳輸的資料量正相關,資料量小於64KB時在30ms內

降低延遲方法:

  • 去掉不必要的事件繫結(WXML中的bindcatch),從而減少通訊的資料量和次數;
  • 事件繫結時需要傳輸targetcurrentTargetdataset,因而不要在節點的data字首屬性中放置過大的資料。


3.4 避免不當使用頁面滑動(onPageSrcoll)的監聽回撥

每一次事件監聽都是一次檢視到邏輯的通訊過程,所以只在必要的時候監聽pageSrcoll。

  • 只在必要的時候艦艇 onPageSrcoll 事件。
  • 避免在 onPageSrcoll 中執行復雜邏輯。
  • 避免在 onPageSrcoll 中頻繁呼叫 setData。
  • 在某些場景下,比如曝光量統計。通常的做法是,監聽 onPageSrcoll 事件,並不斷查詢元素位置,節點資訊查詢(createSelectorQuery)也是非常耗時的,會影響小程式通訊渲染的效能。在這種情況下,使用節點佈局相交狀態監聽(IntersectionObserver)替代,減少不要的通訊。

而當需要在頻繁觸發的使用者事件(如 PageScroll 、 Resize 事件)中呼叫 setData ,合理的利用 函式防抖(debounce) 和 函式節流(throttle) 可以減少 setData 執行次數。

函式防抖(debounce):函式在觸發n秒後才執行一次,如果在n秒內重複觸發函式,則重新計算時間。
函式節流(throttle):單位時間內,只會觸發一次函式,如果同一個單位時間內觸發多次函式,只會有一次生效。

我們還可以自己設計一個 diff 演算法,重新對 setData 進行封裝,使得在 setData 執行之前,讓待更新的資料與原 data 資料做 diff 對比,計算出資料差異 patch 物件,判斷 patch 物件是否為空,如果為空則跳過執行更新,否則再將 patch 物件執行 setData 操作,從而達到減少資料傳輸量和降低執行 setData 頻率的目的。

// setData重新封裝成新的方法,使得資料更新前先對新舊資料做diff對比,再執行setData方法
this.update = (data) => {    
    return new Promise((resolve, reject) => {        
        const result = diff(data, this.data);        
        if (!Object.keys(result).length) {            
            resolve(null);            
            return;        
        }         
        this.setData(result, () => {            
            resolve(result);        
        });    
    });
}複製程式碼

具體流程如下圖:

淺談小程式效能優化


3.5 使用自定義元件

自定義元件除了有利於程式碼複用,提升開發效率外,還可以有效的提升頁面區域性頻繁更新時的效能。

自定義元件的更新只在元件內部進行,不受頁面其他部分內容的影響,可以大大降低頁面更新的開銷。

小程式自定義元件的實現是由小程式官方設計的 Exparser 框架所支援。

在頁面引用自定義元件後,當初始化頁面時,Exparser 會在建立頁面例項的同時,也會根據自定義元件的註冊資訊進行元件例項化,然後根據元件自帶的 data 資料和元件WXML,構造出獨立的 Shadow Tree ,並追加到頁面 Composed Tree 。建立出來的 Shadow Tree 擁有著自己獨立的邏輯空間、資料、樣式環境及setData呼叫:

淺談小程式效能優化

基於自定義元件的 Shadow DOM 模型設計,我們可以將頁面中一些需要高頻執行 setData 更新的功能模組(如倒數計時、進度條等)封裝成自定義元件嵌入到頁面中。

當這些自定義元件檢視需要更新時,執行的是元件自己的 setData ,新舊節點樹的對比計算和渲染樹的更新都只限於元件內有限的節點數量,有效降低渲染時間開銷。

當然,並不是使用自定義元件越多會越好,頁面每新增一個自定義元件, Exparser 需要多管理一個元件例項,記憶體消耗會更大,當記憶體佔用上升到一定程度,有可能導致 iOS 將部分 WKWebView 回收,安卓機體驗會變得更加卡頓。因此要合理的使用自定義元件,同時頁面設計也要注意不濫用標籤。


3.6 儘可能的減少資料結構的巢狀層數

時間開銷大體上與節點樹中節點的總量成正比例關係。因而減少WXML中節點的數量可以有效降低初始渲染和重渲染的時間開銷,提升渲染效能。

初始渲染時,將初始資料套用在對應的WXML片段上生成節點樹。最後根據節點樹包含的各個節點,在介面上依次建立出各個元件。

重渲染時,對當前節點樹與新節點樹的比較時,會著重比較setData資料影響到的節點屬性。因而,去掉不必要設定的資料、減少setData的資料量也有助於提升這一個步驟的效能。


3.7 key值在列表渲染中的作用

key值在列表渲染的時候,能夠提升列表渲染效能。

小程式的頁面是如何渲染的,主要分為以下幾步:

  • 將wxml結構的文件構建成一個vdom虛擬數

  • 頁面有新的互動,產生新的vdom數,然後與舊數進行比較,看哪裡有變化了,做對應的修改(刪除、移動、更新值)等操作

  • 最後再將vdom渲染成真實的頁面結構

key值的作用就在第二步,當資料改變觸發渲染層重新渲染的時候,會校正帶有 key 的元件,框架會確保他們被重新排序,而不是重新建立,以確保使元件保持自身的狀態,並且提高列表渲染時的效率。

key值如果不指明,預設會按陣列的索引來處理,因而會導致一些類似input等輸入框元件的值出現混亂的問題。

  • 不加key,在陣列末尾追加元素,之前已渲染的元素不會重新渲染。但如果是在頭部或者中間插入元素,整個list被刪除重新渲染,且input元件的值還出現了混亂,值沒有正常被更新 。
  • 新增key,在陣列末尾、中間、或者頭部插入元素,其它已存在的元素都不會被重新渲染,值也能正常被更新。因而,在做list渲染時,如果list的順序發生變化時,最好增加key,且不要簡單的使用陣列索引當做key。


PS:親身體驗的資料,如果你們原來小程式的初次渲染到達260ms左右的話,處理好自定義元件,setData相關的優化等基礎優化,長列表暫未優化的情況下,初次渲染就能提升到 140ms左右。


相關文章