本文主要是根據微信小程序官方優(yōu)化建議和《2018微信公開課第七季上海站·小程序專場》的性能優(yōu)化方案,針對性我們的小程序項目進行性能優(yōu)化實踐,將過程記錄下來,方便以后查看,同時也希望能幫助到其他小伙伴,做好性能優(yōu)化。畢竟一切性能優(yōu)化都是為了更好的用戶體驗。
這些問題的場景都反映了小程序的性能問題,直接影響到用戶體驗。
官方建議從這兩方面進行優(yōu)化:
小程序在整個啟動流程中,一般需要完成幾項工作:
開發(fā)者可以在第2,3去優(yōu)化小程序的啟動性能。
小程序在首次打開時,會去下載并執(zhí)行代碼包,隨著代碼包大小的上升,耗時也會相應增加??梢圆扇∫韵路桨福?/p>
對開發(fā)者而言,能使小程序有更大的代碼體積,承載更多的功能與服務;而對用戶而言,可以更快地打開小程序,同時在不影響啟動速度前提下使用更多功能。
建議開發(fā)者按照功能的劃分,拆分成幾個分包,當需要用到某個功能時,才加載這個功能對應的分包。
我們的一個小程序在兩年多前開始開發(fā)的,在設計之初,我們沒有考慮到這一點,當時也沒有小程序分包的功能。好吧,我們還是迎來了這個問題。
微信小程序在開發(fā)文檔中明確指出,小程序的所有包大小必須限制在2M以內,超過大小,就算在開發(fā)者工具中都不能正常預覽,更不能上傳發(fā)版。解決問題的方法:
將靜態(tài)資源圖片壓縮,因為小程序的壓縮算法對圖片的壓縮微乎其微,于是互,筆者對圖片進行一輪壓縮,并且將重復使用的圖片,進行了公共提取,雖然官方推薦使用網絡圖片,但是還需要去維護靜態(tài)資源,嫌麻煩,就放棄。
將項目中的棄用的頁面,以及不用的三方,進行了一波清除。
很多項目現(xiàn)在都是通過webpack打包成不通的分包,資源懶加載的形式來優(yōu)化,小程序也提供了這個功能:分包,筆者按照按照功能劃分的原則,將同一個功能下的頁面和邏輯放置于同一個目錄下,成為一個分包。
注意:1. 自定義第三方組件,需要放在主包內,miniprogram_npm文件會直接打到主包里;2. 小程序的tab切換頁,必須放在主包里。
分包預下載是為了解決首次進入分包頁面時的延遲問題而設計的。如果能夠在用戶進入分包頁面之前就預先將分包下載完畢,那么進入分包頁面的延遲就能夠盡可能降低。
用戶進行了某個操作,再去下載分包,延遲操作用戶體驗很差,于是乎筆者對上面的分包設置分包預下載。在 app.json
文件中配置:
"preloadRule": {
"pages/work/index": {
"network": "all",
"packages": [
"package-work",
"package-field-statistics"
]
},
"pages/appeal/index": {
"network": "all",
"packages": [
"package-appeal"
]
}
},
復制代碼
這里建議不要一次性把所有分包預下載,這樣的操作同樣回帶來性能問題。
小程序中的某些場景(如廣告頁、活動頁、支付頁等),通常功能不是很復雜且相對獨立,對啟動性能有很高的要求。使用獨立分包,可以獨立于主包和其他分包運行。從獨立分包中頁面進入小程序時,不需要下載主包。
建議開發(fā)者將部分對啟動性能要求很高的頁面放到特殊的獨立分包中。
項目中沒有適合的場景,尚未實踐。
大部分小程序在渲染首頁時,需要依賴服務端的接口數據,接口請求放到頁面的生命周期 onLoad
中,而不是 onReady
里。 `:
監(jiān)聽到頁面加載,就校驗登錄情況,請求頁面數據
onLoad: function (options) {
app.checkAuth((error, token) => {
if (error) {
return
}
// 請求該頁面的數據
})
},
復制代碼
小程序提供了wx.setStorageSync等異步讀寫本地緩存的能力,數據存儲在本地,返回的會比網絡請求快。
登錄成功后將用戶的token,以及用戶信息都可以緩存到本地,記得退出登錄的時候清楚緩存,:joy:。
/**
* 設置本地 token 緩存
* @param {Object} session 服務器返回的數據
* @param {String} session.access_token 存取token
* @param {String} session.refresh_token 刷新token
* @param {String} session.expires_in 有效期限,以秒為單位
*/
export function set(session) {
const localSession = Object.assign({}, session, {
expires_timestamp: getExpireTimestamp(session.expires_in)
});
wx.setStorageSync(SESSION_KEY, localSession);
_token = session.access_token;
}
export function clear() {
wx.removeStorageSync(SESSION_KEY);
clearTimeout(refresh_timer);
_token = null;
}
復制代碼
推薦開發(fā)者延遲請求非關鍵渲染數據,縮短網絡請求時延,與視圖層渲染無關的數據盡量不要放在 data 中,以免傳輸垃圾數據,加快首屏渲染完成時間。
通過id請求詳情的情況,id在渲染層不需要,就可以不把id,定義在data中:
// 原來代碼
data: {
id: ‘’,
// ….
},
onLoad: function (options) {
this.setData({
id: options.id
})
// ….
}
// 改寫后 不把id定義到data中
data: {
// ….
},
app.checkAuth((error, token) => {
const id = options.id === undefined ? '' : options.id;
this.id = id
})
復制代碼
接口返回的數據要做數據處理,不要直接都塞給data,減少冗余數據的雙線程回傳。也是 精簡首屏數據優(yōu)化的一部分。
在小程序啟動流程中,會順序執(zhí)行app.onLaunch, app.onShow, page.onLoad, page.onShow, page.onReady,所以,盡量避免在這些生命周期中使用Sync結尾的同步API,如 wx.setStorageSync,wx.getSystemInfoSync 等。
項目中沒有這樣使用,有先見之明。:smile:
小程序的視圖層目前使用 WebView 作為渲染載體,而邏輯層是由獨立的 JavascriptCore 作為運行環(huán)境。在架構上,WebView 和 JavascriptCore 都是獨立的模塊,并不具備數據直接共享的通道。當前,視圖層和邏輯層的數據傳輸,實際上通過兩邊提供的 evaluateJavascript 所實現(xiàn)。即用戶傳輸的數據,需要將其轉換為字符串形式傳遞,同時把轉換后的數據內容拼接成一份 JS 腳本,再通過執(zhí)行 JS 腳本的形式傳遞到兩邊獨立環(huán)境。
而 evaluateJavascript 的執(zhí)行會受很多方面的影響,數據到達視圖層并不是實時的。
** 常見的 setData 操作錯誤 **
導致了兩個后果:
目前項目代碼還是比較規(guī)范的,我們并沒有把setData當成一個普通的對象去調用,曉得每次使用都需要兩個線程間通信,WebView再去渲染的。哇,好棒。
由setData的底層實現(xiàn)可知,我們的數據傳輸實際是一次 evaluateJavascript 腳本過程,當數據量過大時會增加腳本的編譯執(zhí)行時間,占用 WebView JS 線程。
目前每個接口的數據量并大,數據的量級還沒達到影響腳步執(zhí)行的程度,有需要的話再優(yōu)化吧。
當頁面進入后臺態(tài)(用戶不可見),不應該繼續(xù)去進行setData,后臺態(tài)頁面的渲染用戶是無法感受的,另外后臺態(tài)頁面去setData也會搶占前臺頁面的執(zhí)行。
A頁面上有個定時器,此時打開了B頁面,A頁面的定時器還在運行,繼續(xù)搶占B頁面的資源,B頁面卡頓了,但是并不是B頁面的造成的性能問題,這種問題就不太好排查。希望大家都能做個有始有終的人,定時器不用了要清除。下面demo,定時器在 onHide
時要清除掉。切記切記:point_down:
/**
* 生命周期函數--監(jiān)聽頁面顯示
*/
onShow: function () {
clearTimeout(getTodaytime)
this.updateNowTime()
},
/**
* 生命周期函數--監(jiān)聽頁面隱藏
*/
onHide: function () {
// 取消定時器 防止小程序內存不足,崩潰
clearTimeout(getTodaytime)
},
updateNowTime() {
getTodaytime = setInterval(() => {
const myDate = new Date();
const hours = myDate.getHours())
const minutes = myDate.getMinutes())
const seconds = myDate.getSeconds())
const newTime = hours + ':' + minutes + ':' + seconds;
this.setData({
newTime: newTime
})
}, 1000)
},
復制代碼
項目中展示沒用使用該事件。
在需要頻繁更新的場景下,自定義組件的更新只在組件內部進行,不受頁面其他部分內容復雜性的影響。