房產RN頁面啓動速度優化
導語
自58引入React-Native技術棧後,RN在房產業務線被廣泛使用,業務覆蓋租房、二手房、商業地產各線需求共30餘個。隨着房產對app體驗要求的不斷提高,RN頁面體驗優化亟待處理。
RN頁面因其特有的處理邏輯,相對其他原生頁面在頁面啓動耗時方面差距尤爲明顯,所以啓動速度優化是RN頁面體驗優化的第一步。
加載流程分析
首先看下58RN架構圖:
以上過程可概括爲以下幾個階段:
1)初始化RN環境
2)加載框架JS
3)下載業務JS
4)運行業務JS
5)渲染頁面
下文會按照以上流程探討啓動過程中耗時問題產生原因以及解決方案,這裏先同步幾個概念性的問題:
熱更新:
雖然RN使用Js編寫,但release狀態下,每次訪問RN頁面並不是訪問線上頁面,而是訪問已經存儲在本地的bundle文件。如果沒有熱更新,那麼更新RN頁面只能通過發包解決。我們可以通過更新bundle文件來避免發包更新RN頁面。
增量更新:
RN編譯的bundle體積“比較恐怖”,哪怕僅僅是一個hello world的bundle體積也有1.2M。那麼如果我們RN上線,只要更新一個bundle就會耗費用戶大量流量。不僅如此,因爲bundle文件會內置到apk中,而且每一個RN頁面對應一個bundle文件。那麼apk就會增加(1.2± x N)M。這顯然是我們不能接受的。我們在ReactNative0.44.0版本、0.57.8版本分別採用了不同的解決方案進行bundle的拆分,但思路都是將bundle拆分爲框架JS代碼、業務JS代碼兩本分,下文中會以coreBundle代表框架JS代碼部分,bizBundle代表業務JS代碼部分。
問題排查
啓動過程詳細步驟如下圖所示:
上圖根據Android平臺下58App啓動流程、方法命名所畫,其中節點選擇主要依據啓動流程中必要且邏輯相對閉環的子模塊劃分。
根據上圖節點選取,打印一組實測日誌數據:
2019-12-1218:32:03.094 onCreateView---------------------------載體頁生命週期
2019-12-1218:32:03.106 after inflater-----------------------------載體頁 inflater
2019-12-1218:32:03.107 initData----------------------------------初始化數據
2019-12-1218:32:03.107 initRN-----------------------------------初始化RN
2019-12-1218:32:03.109 createWubaRNImmediately--------------創建WBRN核心類
2019-12-1218:32:03.109 buildReactInstanceManager--------------創建RN核心類
2019-12-1218:32:03.111 buildReactInstanceManager finish--------完成創建RN核心類
2019-12-1218:32:03.114 onViewCreated--------------------------載體頁生命週期
2019-12-1218:32:03.114 loadRelease-----------------------------release模式加載
2019-12-1218:32:03.115 doHotUpdate----------------------------開始熱更新
2019-12-1218:32:03.129 createReactContextInBackground--------預創建ReactContext
2019-12-1218:32:03.211 ReactContext initialized------------------預創建完成
2019-12-1218:32:03.246 showContentAndLoadBundle------------加載業務bundle
2019-12-1218:32:03.250 after loadBuzBundle---------------------業務bundle加載完成
2019-12-1218:32:03.250 startReactApplication[1]-------------------調用js啓動入口2019-12-1218:32:03.094 onCreateView---------------------------載體頁生命週期
以上數據測試環境:58App、Android系統、本地緩存最新bundle、小米Mix3,環境變化可能會導致實測數據產生部分差異。
接下來我們對這部分數據按照同步、異步線程拆分得到以下兩部分並標記差量時間:
進入RN頁面同步執行部分:
2019-12-12 18:32:03.094onCreateView---------------------------------0ms
2019-12-12 18:32:03.106 afterinflater-----------------------------------12ms
2019-12-12 18:32:03.107initData---------------------------------------1ms
2019-12-12 18:32:03.107initRN-----------------------------------------0ms
2019-12-12 18:32:03.109createWubaRNImmediately--------------------2ms
2019-12-12 18:32:03.109 buildReactInstanceManager-------------------0ms
2019-12-12 18:32:03.111buildReactInstanceManager finish-------------2ms
2019-12-12 18:32:03.114onViewCreated-------------------------------3ms
2019-12-12 18:32:03.114 loadRelease----------------------------------0ms
2019-12-12 18:32:03.115doHotUpdate---------------------------------1ms
2019-12-12 18:32:03.246showContentAndLoadBundle------------------131ms
2019-12-12 18:32:03.250 afterloadBuzBundle-------------------------4ms
2019-12-12 18:32:03.250startReactApplication[2]-----------------------0ms
預加載部分
2019-12-12 18:32:03.129createReactContextInBackground------------0ms
2019-12-12 18:32:03.211 ReactContextinitialized----------------------82ms
對於以上過程中相對耗時較多部分,已進行標紅標記,下文中會詳細分析。
1. 初始化RN環境
初始RN環境可以簡單分爲創建RN載體頁,預加載兩部分。
1)創建RN載體頁
由上文測試數據可知:
2019-12-1218:32:03.094 onCreateView
2019-12-1218:32:03.106 after inflater
此過程耗時12ms。
耗時原因爲:通過 IO 讀取 xml 文件、通過反射來創建對應的ViewNative頁面也存在同樣情況,且耗時較短,影響較小,暫不處理。
2)預加載:
增量更新降低了bundle體積的同時爲預加載提供了可能,由上文測試數據可知:
2019-12-1218:32:03.129 createReactContextInBackground
2019-12-1218:32:03.211 ReactContext initialized
此過程耗時80+ms。
包含RN核心類創建和加載coreBundle,由於App啓動時進行預加載,每次進入RN頁面時會使用上次創建好的環境,所以不會對頁面啓動速度產生影響。
2. 加載框架JS
由於預加載機制的存在,coreBundle被提前加載,所以進入RN頁面此過程無明顯感知,但是值得一提的是,由於coreBundle是基於比較或過濾的思想得到的產物,其中並非所有內容都存在依賴關係,所以導致部分對象加載後置,後文會對此詳細說明。
3. 下載業務JS
由上文測試數據可知:
2019-12-12 18:32:03.115 doHotUpdate
2019-12-12 18:32:03.246showContentAndLoadBundle
此過程爲本地緩存bizBundle爲最新的情況,耗時130+ms,且受到網絡情況影響,在本地沒有最新bundle需要下載時耗時增長會更加明顯,由於部分高頻使用業務的更新頻率遠遠小於用戶使用頻率,所以我們採取靜默更新策略,優先使用緩存bundle來優化請求熱更新接口耗時,方案需要同時兼顧緩存命中率、版本覆蓋速度兩項關鍵指標。
靜默更新處理邏輯:
以上爲本地緩存bizBundle可用情況,當有更新時首次進入需要下載bundle。
實測數據:WiFi:1.27s(charles 300+kb/s),256kbps:10s以上(charles 16+kb/s)
環境:58APP、Android、小米Mix3、Bundle ID 158
解決方案:
a. 對於本地有緩存但是緩存不可用的情況,可以考慮diff包增量更新。
b. 對於本地沒有緩存的情況,優先考慮降低業務包的大小。
4. 運行業務JS
React-native系統框架中包含Java、C++、JS三層結構,啓動過程中Java層通過C++層調用JS層,其中C++層主要包含:JSCore、bridge、JSLoader、JSCExecutor,該層對於業務開發者不可見,本文也不對此處展開分析,而是採用Java層中的最後調用時機作爲起始,JS層被調用入口作爲截止時機,計算差值,從而判斷該過程是否有優化必要。
時機選擇:
Java層中的最後調用時機:
((AppRegistry)catalystInstance.
getJSModule(AppRegistry.class)).runApplication()。
JS層被調用入口:
AppRegistry.runApplication。
另外,通過systrace測試
reactRootView.startReactApplication
與
((AppRegistry)catalystInstance.getJSModule(AppRegistry.class)).runApplication()
執行時機相差0.415ms,故使用reactRootView.startReactApplication 代替,便於測試。
實測數據:環境 58APP、Android、小米Mix3,單位ms。
數據分析:
1:NativeInvokeStart=>JSRunning耗時80ms
1&2:引入WBAPP耗時增加30ms
3&4&5&6:業務越複雜,JS文件越多,耗時越長
問題歸納:
. Java層中的最後調用時機到JS方法被調用入口莫名多出80ms
. 引用中間層WBAPP導致加載耗時
. 業務代碼增加導致加載耗時
產生原因與解決方案:
1)Java層中的最後調用時機到JS方法被調用入口莫名多出80ms
產生原因:
分析得到耗時根本原因爲AppRegistry.js類的加載,其中:
constReactNative=require(‘ReactNative’);耗時約60ms、constrenderApplication =require(‘renderApplication’);耗時約15ms。
本類會拆分到coreBundle中,但coreBundle並不是正常打包的結果,裏面存在未被引用的類定義,其中包含AppRegistry.js,只有buzBundle引用這個類,所以耗時被後置。
解決方案:
前置這兩個類的引用時機到coreBundle,即在最後添加
__r(‘node_modules/react-native/Libraries/Renderer/shims/ReactNative.js’);__r(‘node_modules/reactnative/Libraries/ReactNative/renderApplication.js’);
由於沒有coreBundle發佈權限,故直接在啓動時讀取修改過的bundle,並將其寫入data/data/com.wuba/files/opt_rn/test.core.bundle。並將coreBundle的路徑指向新文件路徑,從而繞過npm發佈權限、MD5文件名和內容長度校驗,完成結果驗證。
2)引用中間層WBAPP導致加載耗時
數據對比:
內容 | 加載耗時 |
引用WBAPP | 25ms |
引用WBAPP+house-middleware-sdk | 62ms |
引用WBAPP+house-middleware-sdk、house-middleware-components | 62ms |
產生原因:
sdk、組件庫中包含的能力均由各自的index向外提供,這種方式的好處是開發者可以使用形如WBAPP.XXX來調用或引入中間層中的全部API,而不需要關心提供者是誰,這樣的問題在於引入WBAPP的同時就引入了其中提供的全部能力,即使本業務場景不需要也會無條件引入,從而導致bizBundle體積增大,下載成本提高,加載成本提高。
影響較輕,優先級不高,且house-middleware中組件庫部分已經實現按需引入。
解決方案:
基於babel-plugin-import,對於中間層做按需引入,保證業務開發者使用與改造成本足夠低的同時,實現非必須類文件,不參與打包、加載。
但是這樣有個缺陷,不能自定義路徑,必須在lib文件夾下的文件或文件夾,這樣不能很好的分組,我們的需求是希望能自定義組來區分業務組件和通用組件。那麼如何通過按需引入的方式實現自定義路徑,
具體可以參考:https://www.showdoc.cc/Dugz?page_id=3748379513328446,本文不符贅述。
3)業務代碼增加導致加載耗時
測試數據:
內容 | 增加耗時 |
基於react-navigation構造多頁面bundle | 70+ms |
處理高度問題、獲取初始化參數 | 60+ms |
命中到某個具體業務場景(eg:找室友列表頁) | 90+ms |
使用react-navigation並在入口處建立路由關係,依賴顯示引用 | 110+ms |
產生原因:
a. 引用增加導致
b. 兼容高度、獲取初始化參數等必要操作
解決方案:
a. 對於非多頁面工程,可以優先考慮去掉本層封裝,即不引用react-natigation,對於一個bundle中包含多個頁面的情況目前沒有找到比較好的替代、解決方案,但可以在註冊頁面時選擇跳轉協議命中頁面或默認頁面,其他頁面動態引用,就可以將引用和加載後置。
b. 採用全局注入的方式,注入跳轉協議、httpHeader等必要字段,取締等待異步任務——CatalystInstance.setGlobalVariable,高度問題同理。
5. 渲染頁面
爲了實現頁面“秒開”——用戶最快的開到有效視圖,在業務層我們相繼上線了業務數據緩存、接口拆分、分步渲染策略,對於首頁和非首頁業務也有不同的處理方式。
1) 首頁打開速度優化:
a. 業務數據緩存:
緩存降低了網絡情況對於頁面打開速度的影響,這種影響在弱網情況下尤爲明顯。
b. 接口拆分:
接口返回數據時,有些數據可以較快得到,有些數據相對耗時,一個接口返回時,較快部分數據也要等耗時數據一起返回,所以把一個大而全的接口,拆分成幾個小的相對業務內容完整的接口。
接口間請求依賴、響應依賴、響應順序之間描述是一個相對複雜且通用的底層業務邏輯,爲了更高效、更可靠的、一致性的實現此功能,我們在中間層提供簡潔的封裝。
形如:
其中:
. reqDep用於建立請求發起之間依賴關係
. handleDep用於建立響應之間依賴關係
. handleAfter用於描述響應之間處理順序
注意處理:
. 依賴關係中有環的情況
. 依賴是否可達
c. 分步渲染:
按組件:
數據的分步返回且響應數據的返回順序可控,爲分步渲染提供了一個良好的前提,我們可以把頁面內更靠上,返回速度更快的視圖優先渲染、可見,來製造更快的體驗,如進入列表頁立即展示title、篩選。
按可視範圍:
實測得到創建一個list類型數據observable對象是一個相對較慢的過程,
所以當一個list接口返回數據長度較大,且list的每一行對應視圖較高,頁面展示item個數有限時,限制list長度是一個很好的優化方式——先渲染一部分後再渲染一部分。
2) 非首頁打開速度優化:
非首頁頁面優化方式在數據緩存、接口拆分、分步渲染可見視圖上是基本一致的,最主要的區別爲部分二級頁面的數據可以由前置頁面帶入。
當緩存渲染時間踏進300ms內時,從宏觀角度看整個過程,就已經可以明顯的感 覺到頁面在過場動畫切換過程中已經渲染完成,不過從數據來看,似乎還有壓縮空 間,按照上述策略優化代碼後,經分析可得到過程較長的有:
· 緩存數據讀取: 讀取頭部緩存用了25ms、list緩存80ms。
· 網絡請求: 請求耗時主要受網絡情況影響,可控性較低,請求已經進行耗 時接口拆分,業務數據緩存,可以從一定程度上彌補不足。
·多次渲染: 多次渲染導致JS線層佔用,從而產生耗時。
針對以上問題,給出以下編碼建議,對於可以由底層封裝解決掉的問題,我們會逐漸向中間層完善。
· 緩存讀取耗時問題: 降低緩存數據的大小,list類型可在存儲前做截取,只存首屏可見內容。
實測結果,10條數據20+ms,50條數據100+ms,普通list數據類型,轉observable對象時會遍歷list中的子結構把每一層級的每個節點變成observable對象,經調研,這本身沒有什麼好的優化方案,所以我們採取可見部分與不可見部分的分步處理,
雖然會觸發兩次渲染,但是很大程度上的控制了這部分耗時,兩次渲染會經虛擬dom做對比轉爲局部更新,所以用戶無感。
· 多次渲染: 這裏提到的多次是符合邏輯的多次處理,並非因錯誤或處理不當導致,設計狀態時應該考慮如下幾點:
a. 降低渲染次數,考慮數據結構合併,注意過度合併會導致不必要的更新;
b. 強關聯的內容建議合併處理,弱關聯的內容建議分離;
c. 包含關係賦值順序先內後外。
3)按需加載補充:
上面提及了很多首頁打開速度的優化方式,但是沒有采用按需加載的方式,這裏有 必要解釋一下,打開有緩存的首頁主要問題在於bundle文件的體積,降低bundle大小可以有效提高下載效率、加載效率,所以業務拆分是要必要的,按需加載是一個 在現有工程結構下降低包體積的方式。
工程改造爲按需加載後實測數據:
. 公寓正式包 364kb
. 公寓去掉獨棟包 352kb
. 獨棟按需加載包249kb
簡單計算一下,假設a爲公共依賴,b爲無公共依賴部分除去獨棟相關業務,c爲獨棟相關業務,a+b+c= 364 && a+b = 352 && a+c = 249(a = 237,b = 115,c = 12)。
可以看到,公共依賴佔了絕大部分體積,如果不減少公共依賴,只拆分業務,收益十分有限,另外直接命中非主包頁面的情況等待時間會增加,還會產生兩個或三個loading的情況。
優化效果分析
1. 首頁打開速度優化上線前後對比
開啓靜默更新策略優化後,在不同網絡情況下,頁面啓動時間由變量變爲常量,且速度有明顯提升。
2. 非首頁打開速度優化上線前後對比
開啓緩存、接口拆分、分步渲染等策略後,二級頁面打開速度明顯提升,且首屏展示速度不受網絡情況影響。
3. 靜默更新策略緩存命中率
不同業務場景開啓靜默更新後,緩存命中率約爲60%~90%。
注:產品形態不同導致用戶粘度不同,用戶使用頻率和業務迭代頻率的不同會導致 不同產品靜默更新的緩存命中率不同。
4. 靜默更新策略版本收斂速度
不同業務場景開啓靜默更新後,版本收斂情況約爲首日上線覆蓋到85%,7日內超過90%,並在2周內趨近99%,如果開啓業務控制強制棄用緩存,則可以全量覆蓋(用於處理嚴重線上問題,上圖未包含此情況)
總結
由於ReactNative的實現機制,RN頁面不可能達到原生頁面的啓動速度,但是我們會以原生體驗爲目標,儘量縮小差距,並儘量多的把複雜優化邏輯,封裝在底層或模板化,來保證開發效率。
未來計劃:
· 公共依賴瘦身,業務代碼拆分
· 首頁業務引用後置
· 58APP兩個loading合併推動
· 封裝複雜的優化流程,使業務開發更獨立、更純粹
· 建立監控體系,開發過程中檢驗各流程耗時情況是否符合標準
參考文獻:
1.react-native, https://facebook.github.io/react-native/
2.Mobx Issues, https://github.com/mobxjs/mobx/issues
3.react-navigation, https://reactnavigation.org/docs/en/getting-started.html
4. 基於ReactNative的58APP的開發實踐
https://blog.csdn.net/byeweiyang/article/details/80125527
作者簡介:
杜光中:58同城房產技術部-Android開發工程師。主要負責58和安居客APP租房和商業地產業務的開發和維護工作。
推薦閱讀: