小程序性能優化三板斧

爲何有這篇文章

想看乾貨的能夠直接跳轉到正文 ......css

小程序中心是百度 APP小程序流量分發的入口,從百度我的中心能夠進入。html

image

小程序中心說大不大,說小也不小,屬於麻雀雖小五臟俱全的那種,從 18 年到如今經歷了 2 年的迭代,經手了 20 多任開發,1000 次左右的 commit ,也發展成了一個比較成熟的產品。產品發展到必定階段,就開始呈現出技術上的一些瓶頸,前期爲了快速的上線功能埋下了很多的坑,尤爲是性能上的坑,達到了不可忽視的程度。前端

可是坑嘛,嘛,仍是須要後人一點點填上的,因此因此這個「稍顯稍顯「艱鉅的任務天然而然的落在了接手這個小程序的個人身上,隨後便開始了小程序中心的性能優化之路。git

第三季度對性能優化進行了排期,經歷了一系列「神奇的操做」,小程序中心的 FMP 從 2100ms 下降到了如今的 1300ms。針對小程序性能優化也有了一些經驗,總結了一套方法,在組內作了分享,口若懸河的講了兩個小時,可是也許講的太方法論了些,組內的小夥伴看起來都聽的一迷一迷的。甚至會後仍是會被問「怎麼作才能快速的提高小程序的性能呢???」。github

其實性能提高永遠沒有捷徑,須要分析、優化、實驗、監控,須要一點點積累和深刻。隨着你對項目和性能優化理解不斷深刻,會發現提高性能的手段變得愈來愈豐富,性能數據天然也會跟着上去。但,你可能仍是要問「那麼怎麼作才能快速的提高小程序的性能呢」。web

image
好吧,不裝了,我攤牌了,(敲黑板!)如下是一些簡單有效的方法,並且幾乎能夠無腦應用到全部小程序中npm

什麼?你說你不會?好吧,我把源代碼也給你貼上去了,ctrl+c ctrl+v總會吧!該怎麼作你看着辦。小程序

性能優化的背景

在探討性能優化以前,首先須要須要知道什麼是性能。當咱們討論到性能時,實際上是討論應用在不一樣的環境條件、輸入、外界因素下是否能有一致的、穩定的、快速的響應。咱們不但願用戶由於程序代碼寫法上的問題而致使本身的需求受到影響。咱們但願的是,應用能夠快速的響應、流暢的切換,用戶在知足本身需求的過程當中感受不到停頓和等待。在小程序中,性能能夠收斂於三個指標,FMP白屏率服務可用性,下面講一下這三個指標的意義。後端

FMP: First Meaningful Paint,即首次有意義的繪製。FMP 一般是最重要的指標,標誌了程序在通常狀況下的應用表現,FMP 高了說明程序首次加載時間較長,也就是用戶須要等待較長的時間才能進入到小程序中,在這個過程當中用戶可能就會選擇退出了,FMP 低說明用戶很快就能夠進入到小程序中,給用戶的感受就是快,減小了用戶等待的時間。api

白屏率:用戶觸發頁面打開後,間隔必定時間後仍然沒有任何頁面繪製,則認定爲白屏,白屏率 = 白屏發生 PV / 小程序冷啓動打開 PV。白屏率一般是極端狀況下的應用表現,好比在無網、弱網、後端無返回或返回錯誤狀況下的行爲,雖然大部分狀況下不能給用戶有用的信息,可是須要有兜底的策略防止用戶得不到反饋,若是得不到反饋用戶就會認爲是程序出了問題,他不會去考慮環境的問題,也不會去 debug ,你可能就會所以失去一個用戶。

服務可用性:包括

  1. HTTP請求訪問失敗率:請求後端服務時的失敗率,失敗率 = 請求失敗次數 / 請求數量。
  2. JSError:小程序運行過程當中發生的 JS error。

服務可用性表明了錯誤狀況下的應用表現,錯誤按照來源方簡單分爲兩種,一個是服務器端的錯誤,具體的表現就是HTTP請求失敗,一種是前端的錯誤,也就是JS error。這些錯誤有可能什麼都不影響,但也可能嚴重到致使程序異常不能運行,須要具體問題具體分析。

你能夠在 開發者平臺-開發管理-運維中心

image
看到這三個指標的詳細狀況。咱們能夠看到白屏率和服務可用性其實標誌了應用的穩定性和錯誤/異常場景下的表現,而 FMP ,是在正常的業務場景下最直觀的描述小程序性能的指標,下面咱們就圍繞如何「如何下降小程序 FMP 講一下提高小程序性能的「三板斧」。

第一板斧-斷舍離,減小小程序包體積

咱們知道,小程序在發佈的時候都是先將本地的代碼打個包,而後上傳到服務器,用戶在使用咱們的小程序時首先會先下載代碼包,而後宿主app中的小程序框架【todo,小程序核心是什麼意思??】會根據代碼包進行渲染。用戶的網絡狀況咱們不能控制,但代碼包的大小咱們仍是能夠把控的。減小代碼包體積就是一種最簡單也是最直接的方法【todo,可能會被argue,不少開發者作了體積裁剪,可是並不生效】。

能刪除的資源刪除,實在不能刪除的壓縮

用戶打開小程序時只會看到一個頁面,那麼咱們能夠把其它頁面都刪掉,只保留這一個頁面,這樣FMP就能夠降下去。

image

手動狗頭保命,固然不能這麼作,除非飯碗不想要了...

可是這個思路是能夠借鑑的。事實上,若是你的小程序經歷過了屢次迭代,經手過了不一樣的開發人員以後,你會發現,小程序的功能更完善了,包體積也不斷的增長了,然而,這些頁面這些功能真的都是必須的嘛?在 開發者平臺-數據分析-行爲分析-頁面分析-頁面訪問量

image

能夠看到你的小程序各個頁面流量的狀況,對大部分的小程序而言,流量只集中在少數的幾個頁面上,有些頁面根本沒有流量,那這些沒有流量的頁面與功能是否是也能夠從小程序中摘除呢?固然能夠。

從小見大,沒有用的頁面能夠刪除,沒有用到的資源也能夠從小程序包中刪除,包括自定義組件、npm 包、css、圖片。

在智能小程序開發的過程當中,常常須要引入圖片資源。若是使用圖片不當(過多過大的圖片),在加載時會消耗更多的系統資源,從而影響整個頁面的性能,所以作好圖片優化很是重要。【todo,這個話術不必定合適,能夠參看一下 https://smartprogram.baidu.co... 這篇文章裏的說明 update:已改成「在智能小程序開發的過程當中,常常須要引入圖片資源。若是使用圖片不當(過多過大的圖片),在加載時會消耗更多的系統資源,從而影響整個頁面的性能,所以作好圖片優化很是重要。「】,小程序包中的圖片會隨小程序包一塊兒下載,而這些圖片其實能夠放到靜態資源服務器上,小程序代碼中直接使用圖片地址就好。若是特別須要使用圖片,別忘了在小程序開發者工具-項目信息-本地配置-上傳代碼時開啓圖片壓縮。

將入口頁佔比較高的頁面分到主包,其它頁面分到子包

分包 是小程序官方提供的減小包體積的方法,開發者能夠將智能小程序劃分紅不一樣的子包,在構建時打包成不一樣的分包,用戶在使用時按需進行加載。建議按照 開發者平臺-數據分析-行爲分析-頁面分析-入口頁面次數

image
降序來分包,將作入口頁多的頁面放到主包中,其它的頁面適當的分包便可。

須要注意的是,在分包以後,頁面的路徑也會變化,若是以前某些頁面作過推廣活動,爲了防止用戶找不到頁面,可使用 自定義路由 的功能將原地址映射到新地址上。

第二板斧-存數據,巧用緩存與官方能力

快速的展現首屏是咱們的目的,爲了快速的展現首屏,有些東西要放棄,有些東西要妥協。使用官方提供的性能優化的方法,雖然不是那麼優雅,但確實是提高性能的好手段。而緩存這種用空間換取時間的策略,在性能優化的方法上是真的實用有效。

使用 prelink ,使用 onInit

prelink 只需在 開發者平臺-開發管理-設置-開發設置-服務器配置

圖片

配置,你就能夠獲得 200ms 的提高,這簡直是官方給你的尚方寶劍,用不用看你了。它的原理是提早創建 TCP 鏈接和複用 TCP 鏈接。須要注意的是,配置的請求地址是須要支持 HEAD 類型請求的。

onInit 是官方給你的又一個魔法,只須要把 onLoad() 中的獲取數據的方法在 onInit() 中再進行一遍便可。就這麼簡單。

// 修改前

onLoad() {

this.getPageData();

}

// 修改後

onInit() {

if (!this.onInitLoaded) {

this.onInitLoaded = true;

this.getPageData();

}

},

onLoad(options) {

if (!this.onInitLoaded) {

this.onInitLoaded = true;

this.getPageData();

}

}

緩存 API 端能力

API端能力是小程序提供的不一樣於普通 web 應用的功能,這些功能方便了開發者去實現豐富的應用,但端能力其實是有性能消耗的,和普通的 js 語句相比執行起來要慢一些,爲了抹平這種差別,一些不常變化的 API 端能力結果其實能夠緩存起來,屢次獲取時直接從咱們緩存的數據中獲取

const cached = swan.getStorageSync('apiResultCached') || {};

const promiseCache = new Map();

const MAX_CACHE_TIME = 1000 * 60 * 60 * 24 * 7;

// 緩存方法

function memorize(fn) {

const apiName = fn.name;

return function () {

if (cached[apiName]) {

if (Date.now() - cached[apiName]['__timestamp'] < MAX_CACHE_TIME) {

return Promise.resolve(cached[apiName]);

}

cached[apiName] = null;

}

let promise = promiseCache.get(apiName);

if (promise) {

return promise;

}

promise = new Promise((resolve, reject) => {

fn().then(res => {

cached[apiName] = res;

cached[apiName]['__timestamp'] = Date.now();

swan.setStorage({

key: 'apiResultCached',

data: cached

});

resolve(res);

}).catch(e => {

reject(e);

}).finally(() => {

promiseCache.delete(apiName);

});

});

promiseCache.set(apiName, promise);

return promise;

};

}

function getSystemInfoAPI() {

return new Promise((resolve, reject) => {

swan.getSystemInfo({

success: res => resolve(res),

fail: err => reject(err)

});

});

}

// 這裏只緩存了swan.getSystemInfo,一些其它的API方法,只要是不長變化的均可以緩存起來

export const getSystemInfo = memorize(getSystemInfoAPI);

緩存頁面主數據

若是頁面的數據是靜態的,直接寫到 Pagedata 中便可,但實際大部分狀況是,頁面一部分是前端就能夠渲染的靜態的結構與數據,另外一部分是從後端接口獲取的數據。從後端接口獲取的首屏數據能夠緩存到 storage 中,這樣在第二次加載這個頁面的時候能夠從 storage 中獲取,同時異步發起請求,請求返回後再更新頁面數據。注意,咱們是爲了更快的展示頁面,因此只緩存和加載首屏可見的數據便可,非首屏數據延遲加載

// 從storage中獲取頁面數據

swan.getStorage({

key: 'pageData',

success: res => {

// 若是有緩存且異步請求未返回則使用緩存的數據渲染頁面

if (res.data && !this.requestBack) {

this.renderPage(data);

}

}

});

// 異步發起請求獲取頁面數據

getPageData().then(res => {

this.requestBack = true;

// 請求返回後根據最新數據渲染頁面

this.renderPage(res.pageData);

// 同時緩存頁面數據到storage中

swan.setStorage({

key: 'pageData',

data: res.pageData

});

});

這樣作可能會帶來一個問題,就是頁面數據加載後並不必定是最新的數據,最新的數據從請求獲取到後會刷新頁面的數據。因此,若是你的應用對實時性的要求比較高的話可能並不適合使用這種方法。

第三板斧-輕渲染,只渲染必須的內容

在小程序加載過程當中,邏輯代碼和渲染代碼是分離的,分別由不一樣的線程進行。

image
慢的線程會拖累整個加載的速度,當你的邏輯代碼已經跑的飛起的時候,能夠考慮下是否在渲染的層面有改進的辦法。

減小對渲染有消耗的寫法

小程序自己提供了豐富多彩的用法,包括自定義組件動態庫filtersjs等等,這些功能提高了咱們開發的效率,但另外一方面,多種多樣的功能有可能帶來新的的性能消耗陷阱。你須要在效率和性能之間找尋一種平衡,有哪些用法提高的效率有限而帶來的性能消耗倒是不可忽視的?這須要結合自身業務的實踐,但在 FMP 佔比較高的頁面,這些功能仍是須要慎之又慎。

另外,也須要注意 減小view和text組件的特殊屬性和事件 ,這是很容易忽視的一點,雖然單次使用帶來的性能消耗有限,可是要用到 view 和 text 組件的地方太多了,架不住使用數量的上升帶來質的改變。尤爲是自定義組件中使用了低性能的寫法,由於自定義組件可能會被用到屢次(例如列表項,甚至可能會被用上百次上千次),低性能的自定義組件會帶來成倍的性能消耗。

// 修改前 view 使用了 style 屬性

<view style="height: 20rpx;">熱門榜單</view>

// 修改後 view 使用了 class ,在 css 文件中寫樣式

.title {

height: 20rpx;

}

<view class="title">熱門榜單</view>

分屏渲染

設想一下,當咱們加載一個長度超過一個屏幕的列表時,其實用戶不會看到列表的全部內容,只能看到列表的前幾項,那麼咱們固然能夠只加載列表的前幾項,當用戶滑動的時候再加載剩餘的內容。一樣的,在渲染頁面的時候,咱們也能夠在第一次 setData 時進行數據的分割,只設置首屏可見的數據,延遲設置非首屏數據

// appList是從後端接口獲取的頁面數據 active是當前可見的tab索引

// firstLoadAppList爲計算出的首屏幕數據

const firstLoadAppList = appList.map((item, index) => {

return index === active ? item.slice(0, 10) : [];

});

this.setData({

appList: firstLoadAppList

}, () => {

// 可將完整數據記錄待以後加載

this.appList = appList;

});

取消骨架屏採用漸進式加載

骨架屏 是小程序提供的一種優化用戶體驗的機制,但其實任何渲染都有消耗,骨架屏也是。在骨架屏中寫了複雜的結構甚至動畫效果,反而不利於真正的有意義的頁面快速的加載。固然,骨架屏確實可讓用戶更快的感知到頁面正在加載,因此須要在這之間尋找一種平衡,是須要用戶先看到一個正在加載的頁面,仍是讓用戶更快的看到有意義的有內容的畫面。推薦的一個方案是:

  • 使用官方提供的骨架屏,但簡化骨架屏的框架,減小使用樣式與動畫效果
  • 在真正的頁面渲染中,爲各個部分設置背景色與高度,在 Pagedata 中設置默認值,在還未進行第一次 setData 的時候渲染出頁面的框架。這樣,當頁面數據來了的時候,只是在特定的部分填充值便可。

後記

歡迎在 小程序開發者社區 中提問性能相關的問題,也歡迎在Github上 follow我,我會不按期更新一些前端相關的文章,若是想更深刻的和我討論小程序性能相關的問題,能夠給我發郵件。

相關文章
相關標籤/搜索