移動端ReactNative性能探究

原創:libo前端

關於幀

在最近接觸了rn以後,也算有了一點了解,今天寫的文章就來簡單探討一下關於rn性能的二三事。react

首先在討論移動端的性能以前,都必需要了解幀的概念,幀率是評判移動端應用性能的一個極爲重要的標準。衆所周知,不論是手機仍是電腦或是視頻,都是由一組靜態的圖片以一個穩定的速度快速變化所產生的。咱們把這組圖片中的每一張圖片叫作一幀,而每秒鐘顯示的幀數直接的影響了用戶界面的流暢度和真實感。用iOS舉例,iOS設備提供了每秒60的幀率,這就留給了開發者和UI系統大約16.67ms來完成生成一張靜態圖片(幀)所須要的全部工做。若是在這分派的16.67ms以內沒有可以完成這些工做,就會引起丟幀,使界面表現的不夠流暢。但一般來講,保持一個應用在使用過程當中維持每秒60幀的刷新率是一件比較困難的事情,不論是原生代碼仍是rn,都沒法避免額外的沒必要要的性能開銷,所以人工的干預是必要的。web

在討論rn性能優化的方式以前,咱們先討論一下rn的性能問題會出如今哪裏吧。性能優化

關於RN的原理

首先RN爲咱們在移動端提供了JS的運行環境,因此前端開發者們只須要關心如何編寫JS代碼,畫UI只須要畫到virtual DOM 中,不須要特別關心具體的平臺。網絡

而一樣是運行js,RN與h5頁面有本質的區別,使得rn可以具有相似原生App的外觀和手感,在體驗上遠遠超過h5。RN底層幹了不少不少髒活累活,能夠把咱們寫的JS代碼轉化成了native代碼執行,也就是說RN頁面實際上渲染出來的都是原生組件,再也不是webView裏的東西了。dom

RN的本質是把中間的這個橋Bridge給搭好,讓JS和native能夠互相調用。 異步

企業咚咚20191014171601.png

關於線程

在原生iOS應用中,和js單線程不一樣的一點是,原生應用能夠把網絡請求或是一些與ui渲染無關的操做放到子線程異步執行,以此增長主線程渲染的流暢性,讓主線程儘可能只關注於視圖渲染。這是原生應用性能優化的基本概念。函數

那麼在rn應用中,rn又提供了一個特殊的線程即js線程,專門用來執行前端代碼。因此rn應用中一般會存在以下幾種線程:性能

  1. 主線程:UI線程 ,視圖渲染,處理用戶交互
  2. 子線程: 處理網絡請求,進行耗時操做等
  3. js線程:運行js代碼 ,對大多數React Native應用來講,業務邏輯是運行在JavaScript線程上的。

若是隻進行js的編寫,那麼開發過程當中也是接觸不到上面兩種線程的。但咱們須要瞭解的是:在rn應用運行過程當中,js線程和主線程是在高度協同下工做的,例如,在用戶觸發了UI事件響應時,會在主線程接受事件,在傳遞到js代碼,在js線程處理該事件;在js端發起UI更新時,會同時向native端同步數據和UI結構,在主線程完成更新渲染。優化

更新數據到原生支持的視圖是批量進行的,而且在事件循環每進行一次的時候被髮送到原生端,這一步一般會在一幀時間結束以前處理完(若是一切順利的話)。若是JavaScript線程有一幀沒有及時響應,或者主線程還在忙於上一幀的渲染工做,就被認爲發生了一次丟幀。 例如,若是在一個複雜應用的根組件上調用了this.setState,從而致使一次開銷很大的子組件樹的重繪,那麼,這可能會花費一個較長的時間,好比200ms也就是整整12幀的丟失。此時,任何由JavaScript控制的動畫都會卡住。只要卡頓超過100ms,用戶就會明顯的感受到。

這種狀況常常發生在Navigator的切換過程當中:當咱們push一個新的路由時,JavaScript須要繪製新場景所需的全部組件,以發送正確的命令給原生端去建立視圖。因爲切換是由JavaScript線程所控制,所以常常會佔用若干幀的時間,引發一些卡頓。有的時候,組件會在componentDidMount函數中作一些額外的事情,這甚至可能會致使頁面切換過程當中多達一秒的卡頓。

另外一個例子是觸摸事件的響應:若是你在按鈕的響應方法裏處理一個跨越多個幀的工做,就可能致使按鈕自己的點擊動畫被延遲了(顏色或是透明度的改變)。這是由於JavaScript線程太忙了,不可以處理主線程發送過來的原始觸摸事件。結果就不能及時響應這些事件並命令主線程的頁面去調整顏色或是透明度了。

##關於性能瓶頸 經過上面的幾個例子咱們不難看出一個問題,致使性能受影響的緣由是在重繪上。原生應用的Navigator就從不須要注意卡頓問題。那麼是什麼致使rn Navigator的切換如此緩慢?

首先,native代碼在設備上的運行速度毋容置疑,而JS做爲腳本語言,原本就是以快著稱,也就是說兩邊的獨立運行都很快,如此看來,性能瓶頸只會出如今兩端的通訊上,但兩邊其實不是直接通訊的,而是經過Bridge作中間人,查找、調用模塊、接口等操做邏輯,會產生到能讓UI層明顯可感知的卡頓,也就是說Bridge在繪製過程當中的轉換過程是比較低效的。 這一點咱們能夠從一個例子中看出,當咱們上下滾動ScrollView的時候,不論JavaScript線程繁忙到什麼地步,甚至於JavaScript線程卡住,ScrollView的滑動流暢度都不會受到影響,由於ScrollView運行和滑動動畫是徹底在主線程之上進行的。

然而ScrollView的滾動事件會被分發到JS線程,若是咱們接收每一次滾動事件並在每一次滾動裏觸發一次ui更新,那應用的使用效果簡直是災難性的。每一次ui更新不只意味着js的重繪,還會觸發橋接的代碼裝換,和主線程接受到正確的指令後的UI視圖的生成和渲染。 那麼很顯然,性能控制就變成了如何儘可能減小Bridge的邏輯。 應用在如下3種狀況會須要bridge工做。

  1. UI事件響應:這塊內容都發生在Native端,以事件形式傳遞到JS端,只是一個觸發器,不會有過分性能問題
  2. UI更新:JS是決定顯示什麼界面,如何樣式化頁面的,通常都是由JS端發起UI更新,同時向native端同步大量的數據和UI結構,這類更新會常常出現性能問題,特別是界面複雜、數據變更量大、動畫複雜、變更頻率高的狀況。
  3. UI事件響應+UI更新:若是UI更新改動不大,那麼問題不大。若是UI事件觸發了UI更新,同時邏輯複雜、耗時比較長,JS端和Native端的數據同步可能會出現時間差,由此會引起性能問題。

關於優化的方式

這裏大體介紹一些比較經常使用的函數和方法。

1.使用shouldComponentUpdate和pureComponent:

react應用中的state和props的改變都會引發re-render,shouldComponentUpdate這個函數是render()函數調用前被調用的,他的兩個參數nextProps和nextState,分別表示下一個props和下一個state的值。咱們重寫這個鉤子,當函數返回false時,阻止接下來的render()調用以及組件從新渲染,反之,返回true時,組件向下走render從新渲染。

例如這樣寫:

shouldComponentUpdate(nextProps, nextState) {
	return nextState.b !== this.state.b
}
複製代碼

可使得當先後state或者props值不一致的時候,咱們纔會去進行渲染,從而達到了優化的效果。

或是使用pureComponent,自定義的有狀態組件能夠儘可能繼承自pure component,而再也不是component。在閱讀了官方介紹後,能夠發現說到底它自己只是會自動使用shouldComponentUpdate鉤子的普通Component。若是組件繼承自pureComponent,就無需再寫shouldComponentUpdate的函數了。

2.InteractionManager: Interactionmanager 本質上一個延遲計劃函數,它能夠自動將一些耗時較長的工做安排到全部互動或動畫完成以後再進行。這樣能夠保證 JavaScript 動畫的流暢運行。 應用這樣能夠安排一個任務在交互和動畫完成以後執行:

InteractionManager.runAfterInteractions(() => {
	 // ...耗時較長的同步執行的任務...
});
複製代碼

3.setNativeProps: setNativeProps方法能夠理解爲web的直接修改dom。使用該方法修改View、Text等RN自帶的組件,就不會觸發組件的componentWillReceiveProps、shouldComponentUpdate、componentWillUpdate等組件生命週期中的方法。

這樣的確會帶來必定的性能提高,同時也會使代碼邏輯難以理清,並且並無解決從JS側到Native側的數據同步開銷問題。所以這個方式官方都再也不推薦,更推薦的作法是合理使用setState()和shouldComponentUpdate()方法解決這類問題。

4.LayoutAnimation: 實現動畫時通常會採用Animated的接口,但Animated的接口通常會在JavaScript線程中計算出所須要的每個關鍵幀,而LayoutAnimation則利用了原生的動畫庫,Core Animation,LayoutAnimation會一次性將想要實現的動畫的參數傳遞給native,再由native完成動畫,動畫過程js線程就再也不參與了。 可是LayoutAnimation只工做在一次性的動畫上,例如若是動畫可能會被中途取消,這種狀況仍是須要使用Animated。

5.少用狀態組件,儘量用無狀態組件,無狀態組件在轉換成爲native視圖時是不會被實例化的,能夠提高性能,但使用的時候也要注意,咱們不能經過ref來獲取對象了。

相關文章
相關標籤/搜索