雙線程下的界面渲染,小程序的邏輯層和渲染層是分開的兩個線程。在渲染層,宿主環境會把WXML
轉化成對應的JS
對象,在邏輯層發生數據變動的時候,咱們須要經過宿主環境提供的setData
方法把數據從邏輯層傳遞到渲染層,再通過對比先後差別,把差別應用在原來的Dom樹上,渲染出正確的UI界面。javascript
由此可得:頁面初始化的時間大體由頁面初始數據通訊時間和初始渲染時間兩部分構成。其中,數據通訊的時間指數據從邏輯層開始組織數據到視圖層徹底接收完畢的時間,傳輸時間與數據量大致上呈現正相關關係,傳輸過大的數據將使這一時間顯著增長。於是減小傳輸數據量是下降數據傳輸時間的有效方式。java
頻繁的去setData
web
若是很是頻繁(毫秒級)的去setData
,其致使了兩個後果:小程序
JS
線程一直在編譯執行渲染,未能及時將用戶操做事件傳遞到邏輯層,邏輯層亦沒法及時將操做處理結果及時傳遞到視圖層;WebView
的 JS
線程一直處於忙碌狀態,邏輯層到頁面層的通訊耗時上升,視圖層收到的數據消息時距離發出時間已通過去了幾百毫秒,渲染的結果並不實時;每次setData
都傳遞大量新數據api
由setData
的底層實現可知,咱們的數據傳輸實際是一次 evaluateJavascript
腳本過程,當數據量過大 時會增長腳本的編譯執行時間,佔用 WebView JS
線程。數組
後臺態頁面進行 setData
緩存
當頁面進入後臺態(用戶不可見),不該該繼續去進行setData
,後臺態頁面的渲染用戶是沒法感覺的, 另外後臺態頁面去setData
也會搶佔前臺頁面的執行。性能優化
在data
中放置大量與界面渲染無關的數據網絡
1. 減小setdata
的數據量app
若是一個數據不會影響渲染層,則不用放在setData
裏面
2. 合併setdata
的請求,減小通信的次數
避免過於頻繁調用setData
,應考慮將屢次setData
合併成一次setData
調用
// 不要頻繁調用setData
this.setData({ a: 1 })
this.setData({ b: 2 })
// 絕大多數時候可優化爲
this.setData({ a: 1, b: 2 })
複製代碼
3. 去除沒必要要的事件綁定(WXML
中的bind
和catch
),從而減小通訊的數據量和次數
4. 避免在節點的data
的前綴屬性中防止過大的數據
5. 列表的局部更新
在一個列表中,有n
條數據,採用上拉加載更多的方式,假如這個時候想對其中某一個數據進行點贊操做,還能及時看到點讚的效果。
- 能夠採用
setData
全局刷新,點贊完成以後,從新獲取數據,再次進行全局從新渲染,這樣作的有點是:方便,快捷!缺點是:用戶體驗極其很差,當用戶刷量100多條數據後,從新渲染會出現空白期。
- 也能夠採用局部刷新,將點讚的
id
傳過去,知道點的是哪一條數據,從新獲取數據,查找相對應id
的那條數據的下標(index
是不會改變的),用setData
進行局部刷新,如此,即可以顯著提高渲染速度。
this.setData({
list[index]=newList[index]
})
複製代碼
6. 與界面渲染無關的數據最好不要放置在data
中,能夠考慮設置在page
對象的其餘字段下
Page({
onShow: function() {
// 不要設置不在界面渲染時使用的數據,並將界面無關的數據放在data外
this.setData({
myData: {
a: '這個字符串在WXML中用到了',
b: '這個字符串未在WXML中用到,並且它很長…………………………'
}
})
// 能夠優化爲
this.setData({
'myData.a': '這個字符串在WXML中用到了'
})
this._myData = {
b: '這個字符串未在WXML中用到,並且它很長…………………………'
}
}
})
複製代碼
7. 防止後臺頁面的js
搶佔資源
小程序中可能有n個頁面,全部的這些頁面,雖然都擁有本身的webview
(渲染層),可是卻共享同一個js
運行環境。也就是說,當你跳到了另一個頁面(假設是B頁面),本頁面(假設是A頁面)的定時器等js
操做仍在進行,而且不會被銷燬,而且會搶佔B頁面的資源。
8. 謹慎使用onPageScroll
pageScoll
事件,也是一次通信,是webview
層向js
邏輯層的通信。此次通信開銷較大,若是考慮到這個事件被頻繁的調用,回調函數若是有複雜的setData
的話性能就會變得不好。
9. 儘量使用小程序組件
自定義組件的更新只在組件內部進行,不受頁面其餘內容的影響,各個組件也將具備各自獨立的邏輯空間。每一個組件都分別擁有本身獨立的數據,setData
調用。
檢查點:是否頻繁去setData
檢查結果:暫無
檢查點:每次setData
都傳遞大量新數據
檢查結果:暫無
檢查點:後臺態頁面進行setData
檢查結果:存在
產生緣由:因爲改操做處於搜索頁面中,在用戶點擊後便馬上返回到上一級頁面進行數據展現,故在後臺態頁面進行setData
,提升跳轉頁面的速度。
問題代碼:
// pages/line/searchResult/searchResult.js
showSearchDetail(e){
...省略代碼
let prevpage = this.getPrevPage()
prevpage.setData({
isInSearch: true,
showResult,
keyWord:res.routeName
})
}
複製代碼
檢查點:在data
中放置大量與界面渲染無關的數據
檢查結果:存在
產生緣由:因爲當前請求的用以查詢線路信息的接口GET/api/Route/List/{cityid}/{pagesize}/{pageno}
不支持分頁請求,會一次性返回全部數據,因此在以前的方案中,爲了減小請求產生的網絡流量,會一次性把全部數據暫存到頁面的一個數組中,(該數組存儲大概600多個對象),而後再根據需求展現部分數據。
問題代碼:
getLinesInformation(cityID) {
return new Promise((resolve, reject) => {
smartProxy.getRequest(`/Route/List/${cityID}/10/0`)
.then(res => {
this.data.lineArray = res
if (this.data.lineArray)
resolve()
else reject()
})
})
},
複製代碼
解決方案:
方法一:用流量換性能 不暫存所有線路的信息,改而每次出現分頁請求時重複請求該api
,更新頁面展現所需的數組的數組。
缺點:重複請求api
獲取相同的數據,浪費流量
優化效果:
在首次跳轉搜索頁時,耗時500ms
,再之後每次跳轉搜索頁耗時90ms
,下拉分頁加載平均400ms
一頁
方法二:改進存儲方案 當請求到線路api
返回的數據後不放置在data
字段,改成設置在page
對象的其餘字段進行存儲。
優勢:減小頁面負擔,優化性能
代碼實現:
getLinesInformation(cityID) {
return new Promise((resolve, reject) => {
smartProxy.getRequest(`/Route/List/${cityID}/10/0`)
.then(res => {
//用lineArray字段存儲請求得來的數據
this.lineArray = res
if (this.lineArray)
resolve()
else reject()
})
})
},
複製代碼
檢查點:不當使用onPageScroll
檢查結果:存在
產生緣由:本意是爲了實現用戶在查看線路搜索結果後返回線路展現主頁時可以返回到上次瀏覽位置,因此使用onPageScroll
事件獲取滾動高度ScrollTop
,而後存儲。但若是經過onPageScoll
事件獲取的話至關於每次混動都會觸發存儲,嚴重影響頁面效果。
問題代碼:
onPageScroll: function (e) {
// 頁面滾動時執行
// console.log(e);
if (e.scrollTop != 0 && !this.data.isInSearch && !this.data.keyWord) {
//設置緩存
wx.setStorage({
key: 'lineSearchScrollTop',
// 緩存滑動的距離,和當前頁面的id
data: e.scrollTop
})
}
},
複製代碼
解決方案:
wx.createSelectorQuery().selectViewport().scrollOffset
獲取滾動條高度,而且只在用戶點擊搜索框跳轉到搜索頁面的時候才調用,減小onPageScroll
事件對頁面性能的影響//獲取滾動條高度
getScrollTop () {
let that = this
return new Promise((resolve, rej) => {
wx.createSelectorQuery().selectViewport().scrollOffset(function (res) {
that.setData({
scrollTop: res.scrollTop
})
resolve()
}).exec()
})
}
複製代碼
在肯定性能指標前,有必要對小程序頁面的生命週期作一個梳理。
在每一個頁面註冊函數Page()
的參數中,有生命週期的方法:onLoad
、onShow
、onReady
、onHide
、onUnload
。
頁面觸發的第一個生命週期回調是onLoad
,在頁面加載的時候觸發,其參數是頁面的query參數,一個頁面只有一次;
接着是onShow
,監聽頁面的顯示,與onLoad
不一樣,若是頁面被隱藏後再次顯示(例如:進入下一頁後返回),也會觸發該生命週期;
觸發onShow
以後,邏輯層會向渲染層發送初始化數據,渲染層完成第一次渲染以後,會通知邏輯層觸發onReady
生命週期,一個頁面只有一次;
onHide
是頁面隱藏但未卸載的時候觸發的,如 wx.navigateTo
或底部tab切換到其餘頁面,小程序切入後臺等。
onUnload
是頁面卸載時觸發,如wx.redirectTo
或wx.navigateBack
到其餘頁面時。
首先,前一個頁面隱藏,在加載下一個頁面以前,須要先初始化新頁面的組件。頁面首次渲染以後,會觸發組件的ready
,最後觸發的是頁面的onReady
,以下圖:
從PageA打開pageB時的生命週期順序
離開當前頁面時,首先觸發當前頁面的卸載onUnload
,接着是組件離開節點樹的detached
。最後顯示以前的頁面,觸發onShow
。以下圖:
從PageB返回到PageA的生命週期順序
切換到後臺時,小程序和頁面並無卸載,只會觸發隱藏。先觸發頁面的onHide
,接着是App的onHide
。以下圖:
切換到後臺時的生命週期順序
切換到後臺時,小程序會先觸發onShow
,以後纔是頁面的onShow
。以下圖:
切換到前臺時的生命週期順序
瞭解了小程序各個階段的生命週期,咱們能夠制定出關鍵節點的性能指標,整理以下表:
若是咱們記錄下每個頁面的優化先後的可交互時間數據,而且對比,能夠很好的分析每個頁面的性能提高有多少,從而判斷本身有沒有在作無用功。
從上面的關鍵性能指標中,抽取可交互時間做爲本次的重要評估指標之一,即從小程序頁面
onload
事件算起,頁面發起異步請求,請求回來後,把數據經過setData
渲染到頁面後,上述一整個流程所花費的時間。
可是,一個小程序項目每每會有不少個頁面,手動記錄每個小程序的首屏時間,很麻煩。
所以,咱們能夠改寫this.setData
方法,加入上報時間點邏輯。
this._startTime = new Date().getTime();
let fn = this.setData;
this.setData = (obj = {}, handle = '') => {
let now = new Date().getTime();
// 上報渲染所須要的時間
log(now - this._startTime)
fn.apply(this, [obj, handle]);
};
複製代碼
另外,還有一些記錄性能指標須要記錄,在本次的班車線路展現頁面中,當用戶下拉觸底時的分頁加載時間和從點擊搜索框到搜索頁面加載出來的時間也是咱們本次重要的性能評估標準。對於這種自定義場景,咱們能夠利用console.time()
和console.timeEnd()
這一對函數來記錄。
測試平臺:小米8 SE、小程序開發工具
測試流程:首頁 -> 線路 -> 下拉觸底 -> 點擊搜索框
測試指標:可交互時間,分頁加載時間、頁面跳轉時間
優化後指標:
平臺 | 可交互時間(ms) | 分頁加載時間(ms) | 頁面跳轉時間(ms) |
---|---|---|---|
小程序開發工具 | 400 | 130 | 180 |
小米8 SE(掃二維碼真機調試模式) | 3000 | 110 | 1000 |
其中,掃二維碼真機調試模式因爲其自己問題,時間變長爲正常現象,在自動真機調試模式中,各項指標恢復正常,但因爲沒有確切數據,故不列入表格中。
自此,線路展現頁面的性能優化完成,在實際優化過程當中,發現性能影響最大的就是下面的問題
onreachBottom
事件被阻塞了,也就是說,要等大概1~2秒纔會去發起下一頁的請求。 取消掉scroll
事件的監聽,性能就大大提高了。歸根結底仍是對小程序的api
不熟悉,爲了得到滾動條高度而頻繁監聽scroll
事件,可謂是本末倒置。