小程序redux性能優(yōu)化,提升三倍渲染速度
很多使用小程序的用戶會反饋,一些小程序會出現(xiàn)卡頓的情況,比如一些電商小程序在打開商品列表需要幾秒的緩沖時間,那么微信小程序開發(fā)需要如何優(yōu)化,下面是這篇小程序redux性能優(yōu)化。
首先了解小程序的工作原理和性能關(guān)鍵點。
1工作原理 (官方說明)
小程序的視圖層目前使用 WebView 作為渲染載體,而邏輯層是由獨立的 JavascriptCore 作為運行環(huán)境。在架構(gòu)上,WebView 和 JavascriptCore 都是獨立的模塊,并不具備數(shù)據(jù)直接共享的通道。當前,視圖層和邏輯層的數(shù)據(jù)傳輸,實際上通過兩邊提供的 evaluateJavascript 所實現(xiàn)。即用戶傳輸?shù)臄?shù)據(jù),需要將其轉(zhuǎn)換為字符串形式傳遞,同時把轉(zhuǎn)換后的數(shù)據(jù)內(nèi)容拼接成一份 JS 腳本,再通過執(zhí)行 JS 腳本的形式傳遞到兩邊獨立環(huán)境。
而 evaluateJavascript 的執(zhí)行會受很多方面的影響,數(shù)據(jù)到達視圖層并不是實時的。
2性能關(guān)鍵點(官方說明)
-
頻繁的去 setData
在我們分析過的一些案例里,部分小程序會非常頻繁(毫秒級)的去setData,其導致了兩個后果:
Android 下用戶在滑動時會感覺到卡頓,操作反饋延遲嚴重,因為 JS 線程一直在編譯執(zhí)行渲染,未能及時將用戶操作事件傳遞到邏輯層,邏輯層亦無法及時將操作處理結(jié)果及時傳遞到視圖層;
渲染有出現(xiàn)延時,由于 WebView 的 JS 線程一直處于忙碌狀態(tài),邏輯層到頁面層的通信耗時上升,視圖層收到的數(shù)據(jù)消息時距離發(fā)出時間已經(jīng)過去了幾百毫秒,渲染的結(jié)果并不實時;
-
每次 setData 都傳遞大量新數(shù)據(jù)
由setData的底層實現(xiàn)可知,我們的數(shù)據(jù)傳輸實際是一次 evaluateJavascript 腳本過程,當數(shù)據(jù)量過大時會增加腳本的編譯執(zhí)行時間,占用 WebView JS 線程, -
后臺態(tài)頁面進行 setData
當頁面進入后臺態(tài)(用戶不可見),不應該繼續(xù)去進行setData,后臺態(tài)頁面的渲染用戶是無法感受的,另外后臺態(tài)頁面去setData也會搶占前臺頁面的執(zhí)行。
3度量性能指標
我們在優(yōu)化性能時,指標是非常重要的,沒有指標,你沒法知道優(yōu)化的點是否有效。不能單憑感覺去優(yōu)化,要根據(jù)指標反饋,明確優(yōu)化的成果。同時,優(yōu)化就像個無底洞,要注意投入產(chǎn)出比。
用戶反饋的卡頓,要么就是js執(zhí)行消耗資源過多導致處理器沒響應,要么是UI渲染消耗資源過多,導致UI沒法響應用戶操作。
通過查看代碼,我們并沒有消耗大量計算資源的業(yè)務邏輯,但是出現(xiàn)了UI反復操作和搶占資源的現(xiàn)象。
4如何度量
可以利用setData的第二個參數(shù),傳入callback函數(shù),統(tǒng)計渲染時長。代碼如下
- let startTime = Date.now()
- this.setData(data, () => {
- let endTime = Data.now()
- console.log(endTime - startTime, '渲染時長')
- })
案例分析 1、檢查點:是否頻繁去setData 檢查結(jié)果:存在 產(chǎn)生原因:redux中監(jiān)聽的是整個store,只要store變化,就會執(zhí)行setData操作,這就意味著頁面無關(guān)的數(shù)據(jù)改變,也會觸發(fā)該頁面執(zhí)行setData操作,但是這個操作是無意義的。 問題代碼:
- // libs/redux-wechat/connect.js
- // 對整個store進行subscribe。變化就執(zhí)行handleChange
- this.unsubscribe = this.store.subscribe(handleChange.bind(this, options));
- function handleChange(options) {
- ...省略代碼
- const state = this.store.getState()
- const mappedState = mapState(state, options);
- this.setData(mappedState)
- }
解決方案:
只監(jiān)聽當前頁面用到的store中的部分數(shù)據(jù),只有該部分數(shù)據(jù)變化,才setData。(store沒提供單個數(shù)據(jù)的監(jiān)聽,如果自己修改redux實現(xiàn),難度較大,同時修改太底層,容易出不可預料的異常。) 判斷頁面數(shù)據(jù)與需要更新數(shù)據(jù)是否相同,如果相同,不做操作。(這個方案成本比較低,就用它吧)
代碼實現(xiàn):
- // libs/redux-wechat/connect.js
- // 如果更新的數(shù)據(jù)和頁面數(shù)據(jù)相同,不做操作。
- function handleChange(options) {
- ...省略代碼
- const state = this.store.getState()
- const mappedState = mapState(state, options);
- // 如果更新的數(shù)據(jù)和頁面數(shù)據(jù)相同,不做操作。
- if (mappedState === this.prevState) return // 新加入代碼
- this.setData(mappedState)
- // 保存上一次數(shù)據(jù)
- this.prevState = mappedState // 新加入代碼
- }
另外一個優(yōu)化:如果store數(shù)據(jù)毫秒級變化怎么辦,例如更新購物車的同時,還更新了購物數(shù)量,能不能把兩次變化合并起來?因為store的數(shù)據(jù)是共享的,最后一次的更新就是最新的數(shù)據(jù),可以采用節(jié)流器對請求進行合并。
- clearTimeout(this.setDataTMO)
- this.setDataTMO = setTimeout(() => {
- this.setData(mappedState)
- }, 50); // 時間可以看情況調(diào)整
2、檢查點:每次 setData 都傳遞大量新數(shù)據(jù) 檢查結(jié)果:存在 產(chǎn)生原因:
頁面存在引用沒用到的store數(shù)據(jù)。 后端返回數(shù)據(jù)直接進入store,后端接口返回冗余字段。
問題代碼:
- /pages/user/index.js
- connect(state => ({
- member: state.member,
- mycoupon: state.mycoupon,
- guessLikeList: state.recommend.guessLikeList,
- locationInfo: state.common && state.common.locationInfo, //可刪除
- selectedseller: state.home.selectedseller,//可刪除
- carts: state.carts.carts,//可刪除
- ...state.common
- }))
解決方案:
刪除頁面無用的connect (老業(yè)務在使用,修改存在風險,通過后續(xù)迭代優(yōu)化) 請求后端接口后,拿到數(shù)據(jù)進行優(yōu)化處理再把數(shù)據(jù)傳入store(成本較高)
3、檢查點:后臺態(tài)頁面進行 setData 檢查結(jié)果:存在 產(chǎn)生原因:redux connect設計與小程序有差異 問題代碼:
- // libs/redux-wechat/connect.js
- function onLoad(options) {
- ...省略部分代碼
- if(shouldSubscribe){
- this.unsubscribe = this.store.subscribe(handleChange.bind(this, options));
- handleChange.call(this, options)
- }
- }
- function onUnload() {
- ...省略部分代碼
- // 頁面onUnload時,才解除監(jiān)聽
- typeof this.unsubscribe === 'function' && this.unsubscribe()
- }
小程序生命周期中,onUnload會在頁面銷毀時執(zhí)行,例如A->B->C->D 的跳轉(zhuǎn),A頁面一直在監(jiān)聽store的變化,如果D頁面修改數(shù)據(jù),會造成A,B,C頁面也執(zhí)行setData操作,搶占了D的資源,因此造成卡頓。 解決方案:
后臺狀態(tài)的頁面在setData時直接return(目前采用該方法) 當頁面隱藏時,移除監(jiān)聽。
代碼實現(xiàn):
- // 因為在后臺的頁面setData會搶占前臺資源,所以在后臺的頁面不要執(zhí)行setData操作
- if (this.route !== _getActivePage().route) return
但是由于在后臺的頁面數(shù)據(jù)沒法更新,如果D頁面修改A引用的數(shù)據(jù),就會出現(xiàn)A引用舊數(shù)據(jù)問題,所以在onShow的時候做一次同步。
- // 后臺的頁面切換到前臺的時候,做一次數(shù)據(jù)同步
- function onShow(options) {
- if(shouldSubscribe){
- handleChange.call(this, options)
- }
- if (typeof _onShow === 'function') {
- _onShow.call(this, options)
- }
- }
指標測試 做了這么多,到底有沒用,拿出來溜一溜就清楚了。 測試平臺:iphone7、三星s7 、小程序開發(fā)工具 測試流程:首頁 -> 配送到家 -> 加入購物車 -> 結(jié)算 ->查看訂單 測試指標:調(diào)用setData次數(shù),渲染總耗時,平均單次渲染耗時
優(yōu)化后setData次數(shù)平均下降150次。 渲染耗時越是卡頓的機器,收益越大,三星s7平均每次渲染耗時降低826ms。
HiShop小程序工具提供多類型商城/門店小程序制作,可視化編輯 1秒生成5步上線。通過拖拽、拼接模塊布局小程序商城頁面,所看即所得,只需要美工就能做出精美商城。更多小程序商店請查看:小程序商店