騰訊優測優分享 | 探索react native首屏渲染最佳實踐
騰訊優測是專業的移動云測試平臺,旗下的優分享不定時提供大量移動研發及測試相關的干貨~
此文主要與以下內容相關,希望對大家有幫助。
react native給了我們使用javascript開發原生app的能力,在使用react native完成興趣部落安卓端發現tab改造后,我們開始對由react native實現的界面進行持續優化。目標只有一個,在享受react native帶來的新特性的同時,在體驗上無限逼近原生實現。
作為一名前端開發,本文會從前端角度,探索react native首屏渲染最佳實踐。
##1.首屏耗時計算方法
###1.1我們關注的耗時
優化首屏渲染耗時,需要先定義首屏耗時的衡量方法。將react native集成至原生app中時,可以將首屏耗時定義為如下
首屏耗時=react native上下文初始化耗時+首屏視圖渲染耗時
其中,react native上下文初始化耗時為一個固定開銷,通過將初始化過程提前至app啟動后異步進行,在安卓端,這一耗時已經可以降低到約70ms。本文關注的是首屏視圖渲染耗時,文中優化探索是在安卓端react native結合版app中進行,但其思路和方法,可以復用至iOS端。
###1.2渲染耗時衡量方法
關注首屏視圖渲染耗時,需要理解react框架視圖渲染流程,相應的需要了解其生命周期方法。下圖是一張react組件完整的聲明周期圖,從圖中可以看出,上方虛線框內為生命周期第一階段,這個階段完成初始化,并第一次渲染組件。左下角虛線框為組件運行和交互階段,這里可能會再次渲染組件。右下角虛線框為第三階段,組件這一階段卸載消亡。
![](http://img.blog.csdn.net/20160906144533144?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
對應上述生命周期方法,我們在初始階段首先渲染loading視圖,并開始拉取數據。獲取數據后,通過改變狀態(state),觸發視圖的再次渲染,在屏幕繪制出視圖。
![](http://img.blog.csdn.net/20160906144552822?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
結合上述分析,我們將首屏視圖渲染耗時定義為如下
首屏視圖渲染耗時=compontDidUpdate結束時間 – compontWillMount開始時間
##2.開啟渲染優化探索之路
###2.1輸出當前渲染耗時
既然已經明確了視圖渲染耗時的計算方法,那么就可以打時間點日志輸出這一耗時看看。這里查看耗時有兩種方法。
使用調試模式,通過console.log,在chrome調試工具中輸出時間差值
封裝native層日志方法封裝給react native層調用,將日志輸出到app日志文件中。
推薦使用第二種方法,采用這種方法,可以以release模式運行app,輸出結果與我們普通的安裝運行app表現一致。
這時我們的耗時輸出是怎樣的呢?查看日志得知,在wifi環境下,榮耀4X手機上,渲染耗時為約700ms,加上react native上下文初始化時間約70ms耗時,整個界面的耗時需要約770ms。
那么原生app的實現中,發現tab渲染耗時是多少呢?通過打點發現,安卓端原生app實現中,發現tab從onCreateViewEx開始至onResume結束,耗時約100ms。
###2.2來吧,加上緩存
看到上述數據時,我的內心是有點崩潰的,說好的高性能呢?但是我并不慌,根據我的前端開發經驗,我知道有一個大招還沒有用上,那就是使用緩存數據。
react native為我們提供了AsyncStorage模塊,AsyncStorage是一個簡單的、異步的、持久化的Key-Value存儲系統,它對于App來說是全局性的。相對于之前我們使用LocalStorage存儲數據,在react native端,我們可以使用AsyncStorage作為數據存儲解決方案。
在使用緩存之前,我們的視圖渲染流程如下所示
![](http://img.blog.csdn.net/20160906144632214?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
![](http://img.blog.csdn.net/20160906144645457?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
上述流程中,每次渲染界面視圖前,都需要等待網絡請求。這里我們將請求數據緩存起來,渲染界面視圖時,首先使用緩存數據渲染視圖,同時發起網絡請求,數據返回時再以新的數據渲染一遍。使用緩存之后的渲染流程如下圖所示
![](http://img.blog.csdn.net/20160906144645457?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
上述流程中,當有緩存數據時,我們會快速拿到緩存數據渲染出界面視圖。然后在網絡請求返回數據時,再次觸發render方法,以新數據再次渲染視圖。
這里為了提升性能,避免多余的觸發render方法,可以在shouldComponentUpdate方法中判斷cache data和response data差異,僅當兩份數據不一致時才再次觸發render方法。同時,得益于react 框架的虛擬dom特性,在網絡請求的數據返回再次觸發render方法后,react native會計算dom diff并以此為依據來判定視圖更新范圍。
此時通過日志數據可以看到,在有緩存場景下,我們獲取緩存時間耗時約在40ms,此時我們的渲染耗時下降至400ms。
###2.3接管輪播圖
加入緩存優化后,我們將渲染耗時降低至400ms,但是仍與原生實現中耗時有較大差距。此時的優化進入了深水區,經過梳理界面視圖,可以將視圖劃分為上部輪播圖、中間部落列表和下部熱門帖子三個模塊,逐一優化。
![](http://img.blog.csdn.net/20160906144703074?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
在最初的版本中,我們是這么實現輪播組件的:在請求的返回結果中,取出輪播圖集合,依次生成全部圖片視圖,并添加至輪播圖容器視圖中。最后監聽容器視圖的對應事件,設置當前顯示的圖片視圖。
如下圖所示,當結果數據中共有五張輪播圖時,會有五個圖片視圖被添加至輪播圖容器中。初始時僅首張圖片視圖可見,容器內滑動事件發生時,圖片視圖可見狀態隨之改變。
![](http://img.blog.csdn.net/20160906144716246?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
從上述過程可以看出一個明顯的問題:對首屏來講,輪播圖容器內,非可見狀態的其余圖片視圖的渲染是沒有意義的。
既然已經找到了問題,優化是思路也就很自然:在從請求結果中獲取輪播圖集合后,不是生成全部圖片視圖,而是僅生成一份,將這一個圖片視圖添加至容器內,并為他設置當前圖片的url地址。當容器內滑動事件發生時,不再是切換不同視圖的可見狀態,而是復用這一個圖片視圖,切換視圖圖片的url地址,其流程如下圖所示。
![](http://img.blog.csdn.net/20160906144731934?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
采用這一方案,完全去除了非可見狀態的圖片視圖的渲染耗時,同時,通過復用視圖,也降低了對內存的占用。這一方案在理論上將渲染耗時和內存占用做到了最優,然而在實際運用中,會有一個體驗的問題:當滑動發生時,才加載下一張圖片并刷新圖片視圖,會導致容器內滑動事件的卡頓,使得滑動有阻滯感。
因此我們最終采用了一種折衷方案:當輪播圖集合超過三張時,在容器內加入當前圖片視圖,同時提前加入當前圖片的上一張和下一張圖片視圖。容器內滑動事件發生時,復用這三個視圖,來設置這次事件對應的當前圖片、上一張和下一站圖片視圖。
![](http://img.blog.csdn.net/20160906144750341?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
通過日志輸出優化前后的耗時數據,采用這一方案時,在渲染耗時上降低了約30ms,此時我們的首屏渲染耗時下降到了約370ms。
###2.4為列表加特效
在發現tab界面中部,是子類別的部落列表。子類別數量通常是兩個,每個子類別下一般有七至八個列表項,每個列表項由部落圖標和部落名稱組成。
![](http://img.blog.csdn.net/20160906144856380?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
最初的版本中,我們從請求結果數據中獲取子類別下部落集合,以ScrollView為容器,依次創建列表項視圖并添加至容器中。這樣,當所有列表項渲染完成,該子類的部落列表才渲染完成,最初的實現示意如下所示。
![](http://img.blog.csdn.net/20160906144947360?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
通過上述過程可以發現,在部落列表渲染過程中,等待非可視區域的列表項的圖片下載再渲染該視圖,對于首屏是沒有意義的。
結合對輪播圖組件的優化經驗,我們嘗試延遲加載列表項視圖。理想的狀態是在首屏僅渲染可見的幾個列表項,非可見區域的列表項,延遲渲染,流程如下所示
![](http://img.blog.csdn.net/20160906145004844?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
很快我們遇到了問題,在輪播圖組件中,首屏僅顯示第一張圖片。但是在橫向部落列表中,首屏應該渲染幾個列表項呢?答案是我們無法提前確定,首屏可見的列表項,只有在渲染時,根據當前列表項在橫向列表中的相對位置,才能計算出該項是否可見。
繼續思考,雖然我們不能預知在首屏應該幾個列表項,但是利用react native 提供的API方法,我們可以獲取當前視圖相對于容器的坐標位置。
![](http://img.blog.csdn.net/20160906145018813?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
利用measureLayout方法,在視圖完成布局,觸發其onLayout事件回調后,可以通過視圖的measureLayout方法,傳入容器節點,從而獲得當前視圖在容器中的相對位置。利用這一特性,結合我們的視圖特點——列表項視圖的尺寸是確定的,可以在初始時,將所有列表項視圖渲染為這一特定尺寸的空視圖,待所有空視圖渲染完成后,獲取視圖在容器中相對位置,僅將可視區域內的列表項用真實視圖重新渲染。
同時監聽列表容器的滾動事件,在滾動事件回調中,計算視圖在容器中的相對位置,當視圖進入了可視區域時,再用真實視圖替換空視圖,上述流程如下所示
![](http://img.blog.csdn.net/20160906145129174?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
通過上述方案,我們可以實現列表項的延遲渲染特性,通過日志輸出耗時時間,可以發現通過這一優化,渲染優化又可降低約30ms。
上述方案中,屏幕在首次渲染空白視圖和再次渲染首屏可見列表項視圖之間,有短暫閃動的感覺,為解決這一體驗問題,可使用占位圖方案替代空視圖方案,即首先都用靜態資源占位圖來渲染列表項icon圖,并延遲加載非可見列表項icon圖,其方案如下所示
![](http://img.blog.csdn.net/20160906145141288?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
實現效果如下,首次渲染時均用占位圖,首屏區域內的列表項icon視圖依次下載并重新渲染。在采用這樣實現后,解決了空視圖帶來的屏幕閃動問題,快速的完成初次渲染,實現了icon圖片的延遲加載,渲染耗時稍有增加,相比空視圖方案耗時增加約10ms,此時我們的首屏渲染耗時下降到了約350ms。
###2.5熱門帖模塊的歸宿
發現tab視圖下部還有一個模塊,如下所示,是幾條熱門帖子的展示區域。對于首屏內容來說,這一模塊整個都屬于非可視區域,因此我們需要設法將這一塊的耗時從首屏耗時中拿掉。
![](http://img.blog.csdn.net/20160906145241644?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
有了上面部落列表的優化經驗,我們已經知道,在布局事件完成后,可以獲取視圖在容器內的相對位置,對于滾動視圖如ScrollView,我們可以監聽他的滾動事件,來判斷視圖距離可視區域的位置以決定是否渲染該視圖。
為了優化體驗,我們再給視圖的渲染設置一個提前閾值aheadDistance,當視圖距離可視區域還有aheadDistance單位時,提前渲染該視圖,這一方法如下圖所示
![](http://img.blog.csdn.net/20160906145230910?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
采用這一方案后,已經可以將熱門帖子模塊的渲染耗時從首屏耗時中去掉了,此時通過日志輸出耗時分析,首屏渲染耗時下降到了約270ms。
那么這塊的優化工作完成了嗎?沒有,在優化做完,回顧體驗時,我們發現了這里仍存在體驗問題。這一體驗問題與發現tab的視圖結構有關。在發現tab中,熱門帖子模塊,距可視區域底部的距離不遠。因為本身距離可視區域底部很接近,所以ahead Distance的設置并沒有起到預想的效果,導致發現tab界面向下滑動時,因為實時去渲染熱門帖子模塊,使得滑動不流暢,有阻滯感。
為了解決這一體驗問題,繼續對熱門帖子模塊做了定制的優化,最終對熱門帖子模塊做到了異步渲染。在首屏渲染時,還是將熱門帖子渲染為空視圖,然后將ahead Distance調大,使得空視圖落在ahead Distance區域內,接著使用setTimeout技巧,異步的去計算該空視圖在容器中位置,并將熱門帖子真實視圖渲染出來。
通過上述方案,將延遲渲染替換為異步渲染,在不增加渲染耗時的基礎上,同時解決了滑動不流暢的體驗問題,最終將首屏渲染耗時定格在約270ms。
##3.總結
react native框架給了我們新的能力,使得我們可以用javascript開發原生app。當我們走入react native應用的深水區,開始對他進行各方面細致的優化時,我們在原來web端積累的最佳實踐依然有效,諸如緩存的使用、元素復用、延遲加載、異步加載等方法依然會起到很好的效果。
與此同時,對渲染耗時的優化也要兼顧使用的體驗,我們應該追求的,不僅僅是快。而是在快的基礎上,也有很好的使用體驗。
優化探索到最后,我們將首屏渲染耗時定格在約270ms,加上react native初始化耗時約70ms,我們的首屏總耗時仍需約340ms,相比原生實現仍然存在差距。原生實現的渲染速度也是經過了層層的優化,而我們對react native最佳實踐的探索也沒有結束,歡迎大家指導、探討。
文 / 騰訊 龔麒
_______________________________________________________________________________________
騰訊優測是專業的移動云測試平臺,為應用、游戲、H5混合應用的研發團隊提供產品質量檢測與問題解決服務。不僅在線上平臺提供自動化兼容性測試、云手機遠程租用與調試、漏洞分析、自動化測試工具Xtest等多種質量檢測工具,更為VIP客戶配備了專家團隊提供定制化綜合測試解決方案。