寶寶知道小程序從首次發佈至今,通過了幾十個版本的迭代。隨着業務發展,頁面功能內容的不斷增多,相關性能數據不斷變差,核心性能數據 FMP 長期處在 2000ms 以上。小程序
在該項目以前,咱們團隊也對小程序作了必定的性能調優工做,內容包括:後端
通過上述手段以後,FMP 降到了 1900ms 左右,後續再也沒法產生優化效果。api
以上優化手段,基本排除了網絡鏈接,包體積優化不到位引發的性能不佳。那麼咱們就只有一個問題須要仔細排查 —— 內容的渲染效率。網絡
目前從手百上打開寶寶知道小程序的最大入口頁面爲問答頁,總體 pv 佔比超過 6 成,那麼咱們優先優化這個頁面,即可以帶來性能收益的最大化。框架
通讀問答頁代碼,按顯示順序從上到下,整個頁面的功能點依次爲:異步
須要展示的內容類別不少,內容信息量較爲龐大。部份內容須要單獨接口獲取,外加上引入的廣告組件,展示效率徹底沒法優化。ide
由於以上業務內容的展示須要,在加載時,使用 setData 觸發內容渲染,會形成較大問題,好比:工具
以上兩條 setData 的使用問題,在配置較好的手機設備上,並不會體現出問題,可是對於中低配置的手機設備,由於操做擁塞或大量數據渲染操做帶來的渲染延遲,形成的用戶體驗損失仍是很大的。性能
優化以前,頁面加載完數據以後的首次渲染,會一次提交問題區、回答區、廣告組件區三個部分的渲染任務,因爲這三個區域涉及的內容量比較大,基本都會超過一屏,甚至兩屏以上,另外各個區域也都包含一些圖文內容,加上自己耗時較高的廣告組件。總體頁面內容渲染速度不好。而且,由於存在直播信息橫條等單獨異步請求加載的數據內容渲染,也容易形成 setData 操做在小程序渲染線程中擁塞現象的發生。優化
因此,從小程序 FMP 的統計規則來看,目前的數據渲染邏輯,顯然並非最優的。
既然 FMP 主要統計的是用戶第一眼能夠看到的首屏位置內容,那麼咱們是否是能夠換個思路來完成咱們的內容渲染工做。
在確保數據接口性能已經符合常規標準的狀況下,咱們可使用更聰明的渲染策略。
爲了解決上述問題,咱們構思了一套分屏式內容渲染策略,意在讓用戶能最快速度的先看到一部分關鍵內容,再分階段渲染剩下須要被渲染的數據,而那些不須要被自動渲染的數據,能夠改爲由用戶某種行爲(好比滑動頁面)觸發加載和渲染。
PS:廣告組件自己爲異步組件,第二次 setData 會觸發廣告組件渲染,而廣告組件內部自行發起異步內容的加載。
優化後的問答頁渲染邏輯,總體上被拆分爲四個階段:
PS:若是存在覈心內容渲染完成後依舊沒法撐滿一屏的狀況,能夠考慮設置總體頁面 min-height:100vh ,或者頁面下方放置佔位元素,來達到撐滿一屏的效果。
該優化版於2020年8月4日上午11點左右全量上線,在手百中逐步放量。 FMP 指標在8月5日和6日兩天快速降低,7日逐步穩定。總計優化 FMP 指標 540ms 。
從數據表現來看,優化效果很是明顯。
而且,問答頁做爲寶寶知道小程序 pv 最大的落地頁,佔據總 pv 的 60% 左右,另外還有 40% 的其餘頁面須要咱們持續優化,將來數據表現還有不小的優化空間。
工欲善其事必先利其器。後續咱們還須要優化其餘入口頁面的性能,以及爲後續開發高性能頁面作持續的技術儲備,因此咱們將開發中遇到的和性能有關的問題作了一些抽象,經過打造基礎操做的工具類庫,從底層上來解決或者規避問題。
上文中有提到,同時發起多個 setData 操做,極易形成小程序渲染線程的擁塞,致使渲染效率受到影響,下降小程序內容上屏的效率。實際開發中,咱們若是要避免同時發起多個 setData ,必然會帶來額外的邏輯思考成本和代碼結構調整的成本,也容易由於調整,下降代碼的可讀性和可維護性。爲了兼顧渲染性能的須要和代碼結構的可讀性,以及代碼觀感,咱們專門設計了一個內容渲染任務管理器。
DataSetter
DataSetter 目前已經集成在團隊內部的小程序工程腳手架中,經過 AdvancedPage 建立的小程序 Page 實例,便可支持經過該管理器開放的 api 接口,向小程序的渲染線程提交數據渲染任務。
DataSetter 將小程序 setData 操做封裝爲一個隊列式的渲染任務管理器,使用 DataSetter 進行 set 數據操做,可使得單位時間內只有一個 setData 操做被執行,而其餘被同時 set 的數據,將在隊列中排隊依次執行。
圖例:優化前同時 setData ,會致使小程序渲染線程的擁塞
圖例:優化後同時 set ,DataSetter 會總體管理數據渲染任務,不會形成渲染線程擁塞
爲了支持分屏式渲染策略的編寫,DataSetter 的 API 被設計爲鏈式調用式設計。能夠以非嵌套的方式編寫N階段內容渲染邏輯,代碼行文清晰易懂。
this.$dataSetter.set({ // 第一階段渲染數據 status:'success', aaa:111 }).done(e => { // 第一階段渲染完成 console.log('第一階段渲染完成'); }).set({ // 第二階段渲染數據 bbb:222 }).set({ // 第三階段渲染數據 ccc:333 }).done(e => { // 第三階段渲染完成 console.log('第三階段渲染完成‘); }); 複製代碼
/** * @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); } }); 複製代碼
形成小程序性能不理想的狀況有不少,而渲染問題的解決和優化是能夠帶來最大收益的,而且若是能根據實際的業務場景,來靈活設計視圖的渲染策略,每每能夠帶來奇效。渲染問題優化是一件很是精細的事情,尤爲是面對逐漸複雜的業務代碼,勇於去改造嘗試,纔是最終成功的起點。