小程序性能優化三板斧

爲什麼有這篇文章

想看乾貨的可以直接跳轉到正文 ......

小程序中心是百度 APP小程序流量分發的入口,從百度個人中心可以進入。

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

但是坑嘛,嘛,還是需要後人一點點填上的,所以所以這個“稍顯稍顯“艱鉅的任務自然的落在了接手這個小程序的我的身上,隨後便開始了小程序中心的性能優化之路。

第三季度對性能優化進行了排期,經歷了一系列“神奇的操作”,小程序中心的 FMP 從 2100ms 降低到了現在的 1300ms。針對小程序性能優化也有了一些經驗,總結了一套方法,在組內做了分享,滔滔不絕的講了兩個小時,但是也許講的太方法論了些,組內的小夥伴看起來都聽的一迷一迷的。甚至會後還是會被問“怎麼做才能快速的提升小程序的性能呢???”。

其實性能提升永遠沒有捷徑,需要分析、優化、實驗、監控,需要一點點積累和深入。隨着你對項目和性能優化理解不斷深入,會發現提升性能的手段變得越來越豐富,性能數據自然也會跟着上去。但,你可能還是要問“那麼怎麼做才能快速的提升小程序的性能呢”。

好吧,不裝了,我攤牌了,(敲黑板!)以下是一些簡單有效的方法,而且幾乎可以無腦應用到所有小程序中 什麼?你說你不會?好吧,我把源代碼也給你貼上去了,~~ctrl+c ctrl+v總會吧!~~該怎麼做你看着辦。

性能優化的背景

在探討性能優化之前,首先需要需要知道什麼是性能。當我們討論到性能時,其實是討論應用在不同的環境條件、輸入、外界因素下是否能有一致的、穩定的、快速的響應。我們不希望用戶因爲程序代碼寫法上的問題而導致自己的需求受到影響。我們希望的是,應用可以快速的響應、流暢的切換,用戶在滿足自己需求的過程中感覺不到停頓和等待。在小程序中,性能可以收斂於三個指標,FMP白屏率服務可用性,下面講一下這三個指標的意義。

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

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

服務可用性:包括

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

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

你可以在 開發者平臺-開發管理-運維中心 看到這三個指標的詳細情況。我們可以看到白屏率和服務可用性其實標誌了應用的穩定性和錯誤/異常場景下的表現,而 FMP ,是在正常的業務場景下最直觀的描述小程序性能的指標,下面我們就圍繞如何“如何降低小程序 FMP 講一下提升小程序性能的“三板斧”。

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

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

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

用戶打開小程序時只會看到一個頁面,那麼我們可以把其它頁面都刪掉,只保留這一個頁面,這樣FMP就可以降下去。 手動狗頭保命,當然不能這麼做,除非飯碗不想要了...

但是這個思路是可以借鑑的。事實上,如果你的小程序經歷過了多次迭代,經手過了不同的開發人員之後,你會發現,小程序的功能更完善了,包體積也不斷的增加了,然而,這些頁面這些功能真的都是必須的嘛?在 開發者平臺-數據分析-行爲分析-頁面分析-頁面訪問量 可以看到你的小程序各個頁面流量的情況,對大部分的小程序而言,流量只集中在少數的幾個頁面上,有些頁面根本沒有流量,那這些沒有流量的頁面與功能是不是也可以從小程序中摘除呢?當然可以。

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

在智能小程序開發的過程中,經常需要引入圖片資源。如果使用圖片不當(過多過大的圖片),在加載時會消耗更多的系統資源,從而影響整個頁面的性能,因此做好圖片優化非常重要。【todo,這個話術不一定合適,可以參看一下 https://smartprogram.baidu.com/docs/develop/performance/performance_img/ 這篇文章裏的說明 update:已改爲“在智能小程序開發的過程中,經常需要引入圖片資源。如果使用圖片不當(過多過大的圖片),在加載時會消耗更多的系統資源,從而影響整個頁面的性能,因此做好圖片優化非常重要。“】,小程序包中的圖片會隨小程序包一起下載,而這些圖片其實可以放到靜態資源服務器上,小程序代碼中直接使用圖片地址就好。如果特別需要使用圖片,別忘了在小程序開發者工具-項目信息-本地配置-上傳代碼時開啓圖片壓縮。

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

分包 是小程序官方提供的減少包體積的方法,開發者可以將智能小程序劃分成不同的子包,在構建時打包成不同的分包,用戶在使用時按需進行加載。建議按照 開發者平臺-數據分析-行爲分析-頁面分析-入口頁面次數 降序來分包,將做入口頁多的頁面放到主包中,其它的頁面適當的分包即可。 需要注意的是,在分包之後,頁面的路徑也會變化,如果之前某些頁面做過推廣活動,爲了防止用戶找不到頁面,可以使用 自定義路由 的功能將原地址映射到新地址上。

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

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

使用 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
    });
});

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

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

在小程序加載過程中,邏輯代碼和渲染代碼是分離的,分別由不同的線程進行。 慢的線程會拖累整個加載的速度,當你的邏輯代碼已經跑的飛起的時候,可以考慮下是否在渲染的層面有改進的辦法。

減少對渲染有消耗的寫法

小程序本身提供了豐富多彩的用法,包括自定義組件動態庫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我,我會不定期更新一些前端相關的文章,如果想更深入的和我討論小程序性能相關的問題,可以給我發郵件。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章