【性能優化實戰】寶寶知道小程序FMP優化實錄

背景

寶寶知道小程序從首次發佈至今,通過了幾十個版本的迭代。隨着業務發展,頁面功能內容的不斷增多,相關性能數據不斷變差,核心性能數據 FMP 長期處在 2000ms 以上。小程序

在該項目以前,咱們團隊也對小程序作了必定的性能調優工做,內容包括:後端

  1. 包體積優化,去除了很多引用在項目中的圖片素材文件,將包體積優化至 500kb 如下;
  2. 聯合後端對耗時較高的業務接口作優化,單個接口返回速度須要控制在 100ms 左右;
  3. 優化了部分業務邏輯,小程序啓動時減小了一些沒必要要的操做邏輯;
  4. 使用了小程序框架提供的最新生命週期 onInit ,可提早 100ms 左右發起業務網絡請求;
  5. 使用 prelink 預鏈接網絡,提高數據接口的請求效率。

通過上述手段以後,FMP 降到了 1900ms 左右,後續再也沒法產生優化效果。api

以上優化手段,基本排除了網絡鏈接,包體積優化不到位引發的性能不佳。那麼咱們就只有一個問題須要仔細排查 —— 內容的渲染效率。網絡

問題發現

目前從手百上打開寶寶知道小程序的最大入口頁面爲問答頁,總體 pv 佔比超過 6 成,那麼咱們優先優化這個頁面,即可以帶來性能收益的最大化。框架

通讀問答頁代碼,按顯示順序從上到下,整個頁面的功能點依次爲:異步

  1. 直播信息橫條
  2. 問題區
  3. 回答區
  4. 廣告組件區
  5. 爲你推薦 feedlist

須要展示的內容類別不少,內容信息量較爲龐大。部份內容須要單獨接口獲取,外加上引入的廣告組件,展示效率徹底沒法優化。ide

由於以上業務內容的展示須要,在加載時,使用 setData 觸發內容渲染,會形成較大問題,好比:工具

  1. 加載期間調用 setData 的頻次過多,onLoad 時會 set 、onShow 時會 set ,不一樣階段發起的異步數據加載後也會 set 。當前線程內同時的屢次 setData ,極易形成小程序渲染線程擁塞,影響內容渲染效率
  2. 單次 setData 數據量過多,接口數據返回後,全部頁面內須要的數據都一次性被提交到渲染線程中渲染,致使線程等待時間長,影響了有效內容的最終展示。雖然減小 setData 調用次數是官方提倡的,可是單次提交過多數據渲染,也並非最優的策略。

以上兩條 setData 的使用問題,在配置較好的手機設備上,並不會體現出問題,可是對於中低配置的手機設備,由於操做擁塞或大量數據渲染操做帶來的渲染延遲,形成的用戶體驗損失仍是很大的。性能

優化前的問答頁數據渲染示意圖

image

優化以前,頁面加載完數據以後的首次渲染,會一次提交問題區、回答區、廣告組件區三個部分的渲染任務,因爲這三個區域涉及的內容量比較大,基本都會超過一屏,甚至兩屏以上,另外各個區域也都包含一些圖文內容,加上自己耗時較高的廣告組件。總體頁面內容渲染速度不好。而且,由於存在直播信息橫條等單獨異步請求加載的數據內容渲染,也容易形成 setData 操做在小程序渲染線程中擁塞現象的發生。優化

因此,從小程序 FMP 的統計規則來看,目前的數據渲染邏輯,顯然並非最優的。

既然 FMP 主要統計的是用戶第一眼能夠看到的首屏位置內容,那麼咱們是否是能夠換個思路來完成咱們的內容渲染工做。

在確保數據接口性能已經符合常規標準的狀況下,咱們可使用更聰明的渲染策略。

優化方案

爲了解決上述問題,咱們構思了一套分屏式內容渲染策略,意在讓用戶能最快速度的先看到一部分關鍵內容,再分階段渲染剩下須要被渲染的數據,而那些不須要被自動渲染的數據,能夠改爲由用戶某種行爲(好比滑動頁面)觸發加載和渲染。

優化後的問答頁渲染示意圖

image
PS:廣告組件自己爲異步組件,第二次 setData 會觸發廣告組件渲染,而廣告組件內部自行發起異步內容的加載。

優化後的問答頁渲染邏輯,總體上被拆分爲四個階段:

  1. 核心內容快速渲染階段。該階段爲 FMP 主要檢測的數據渲染時長,因此在這個階段,咱們須要讓頁面的內容和元素,足夠裝滿一屏。
  2. 核心內容補全渲染階段。該階段將核心內容中存在的耗時內容,好比圖片、視頻以及小程序 native 組件等內容渲染上屏(注:關於渲染比較耗時的組件,目前已知視頻 video 、全部小程序 native 組件,都不適宜放在第一階段直接渲染,圖片 image 若是條件容許,也儘可能不放在第一屏)。
  3. 後續內容渲染階段。該階段將本次接口返回的須要渲染的數據所有上屏。
  4. 其餘非主要異步數據渲染階段(圖例中的直播信息橫條)。將另一個接口的數據渲染上屏。

PS:若是存在覈心內容渲染完成後依舊沒法撐滿一屏的狀況,能夠考慮設置總體頁面 min-height:100vh ,或者頁面下方放置佔位元素,來達到撐滿一屏的效果。

優化成果

該優化版於2020年8月4日上午11點左右全量上線,在手百中逐步放量。 FMP 指標在8月5日和6日兩天快速降低,7日逐步穩定。總計優化 FMP 指標 540ms 。

image
從數據表現來看,優化效果很是明顯。

而且,問答頁做爲寶寶知道小程序 pv 最大的落地頁,佔據總 pv 的 60% 左右,另外還有 40% 的其餘頁面須要咱們持續優化,將來數據表現還有不小的優化空間。

工具建設

工欲善其事必先利其器。後續咱們還須要優化其餘入口頁面的性能,以及爲後續開發高性能頁面作持續的技術儲備,因此咱們將開發中遇到的和性能有關的問題作了一些抽象,經過打造基礎操做的工具類庫,從底層上來解決或者規避問題。

上文中有提到,同時發起多個 setData 操做,極易形成小程序渲染線程的擁塞,致使渲染效率受到影響,下降小程序內容上屏的效率。實際開發中,咱們若是要避免同時發起多個 setData ,必然會帶來額外的邏輯思考成本和代碼結構調整的成本,也容易由於調整,下降代碼的可讀性和可維護性。爲了兼顧渲染性能的須要和代碼結構的可讀性,以及代碼觀感,咱們專門設計了一個內容渲染任務管理器。

DataSetter

DataSetter 目前已經集成在團隊內部的小程序工程腳手架中,經過 AdvancedPage 建立的小程序 Page 實例,便可支持經過該管理器開放的 api 接口,向小程序的渲染線程提交數據渲染任務。

DataSetter 將小程序 setData 操做封裝爲一個隊列式的渲染任務管理器,使用 DataSetter 進行 set 數據操做,可使得單位時間內只有一個 setData 操做被執行,而其餘被同時 set 的數據,將在隊列中排隊依次執行。

圖例:優化前同時 setData ,會致使小程序渲染線程的擁塞

image

圖例:優化後同時 set ,DataSetter 會總體管理數據渲染任務,不會形成渲染線程擁塞

image

爲了支持分屏式渲染策略的編寫,DataSetter 的 API 被設計爲鏈式調用式設計。能夠以非嵌套的方式編寫N階段內容渲染邏輯,代碼行文清晰易懂。

this.$dataSetter.set({
    // 第一階段渲染數據
    status:'success',
    aaa:111
}).done(e => {
    // 第一階段渲染完成
    console.log('第一階段渲染完成');
}).set({
    // 第二階段渲染數據
    bbb:222
}).set({
    // 第三階段渲染數據
    ccc:333
}).done(e => {
   // 第三階段渲染完成
   console.log('第三階段渲染完成‘);
});
複製代碼

DataSetter 源碼

/**
* @name DataSetter
* @description setData語法加強,支持鏈式調用和隊列式set數據,一次setData成功以後纔開始下一次setData
*/
class DataSetter {
    queue = [];  
    context = null;  
    index = 0;  
    constructor(context) {
        this.context = context;
    }  
    set(dataset = {}) {
        this.queue.push({dataset, callback: null});
        if (this.queue.length === 1) {
            this.exec();
        }
        return this;
     }  
     done(callback) {
         this.queue[this.queue.length - 1].callback = callback;
         return this;
     }  
     exec() {
         let task = this.queue[this.index];
         if (!task) {
             // console.log('all task done!');
             this.refresh();
             return;
         }  
        const next = () => {
            // console.log(set data ${this.index} ok!);
            task.callback && task.callback();
            this.index++;
            this.exec();
        };
        // 若是當前任務dataset爲空,則不調用原生setData,直接執行回調
        if (!task.dataset || Object.keys(task.dataset).length < 1) {
            next();
            return;
         }
        // console.log(set data ${this.index});
        this.context.setData(task.dataset, next);
     }  
     refresh() {
         this.queue = [];
         this.index = 0;
     }
}  
// Page Demo  
Page({
    $dataSetter: null,
    onLoad() {
        this.$dataSetter = new DataSetter(this);
    }
});
複製代碼

後記

形成小程序性能不理想的狀況有不少,而渲染問題的解決和優化是能夠帶來最大收益的,而且若是能根據實際的業務場景,來靈活設計視圖的渲染策略,每每能夠帶來奇效。渲染問題優化是一件很是精細的事情,尤爲是面對逐漸複雜的業務代碼,勇於去改造嘗試,纔是最終成功的起點。

相關文章
相關標籤/搜索