React Native 效能之謎

ThoughtWorks發表於2017-04-14

在 PhoneGap、RubyMotion、Xamarin、Ionic 一眾跨平臺開發工具中,React Native能夠殺出一條血路,獲得目前這麼大的影響力,除了React社群生態圈的加持和Facebook的大力推廣以外,另外一個最主要的原因就是其在開發效率和應用效能方面取得了一個比較好的平衡:

  • 開發效率通過JS工程實踐,邏輯跨平臺複用得到極大提升
  • 效能則通過全Native的UI層得到滿足

不過,雖說框架提供了這個平衡能力,平衡點的選擇卻掌握在開發者手中,本文將從React Native的效能角度來看看應該如何掌握這個平衡點。

React Native的工作原理

在React Native的應用中,存在著兩個不同的技術王國:JS王國和Native王國。應用在啟動時會先進行雙向註冊,搭好橋,讓兩個王國知道彼此的存在,以及定義好彼此合作的方式:

(圖片來源:https://tadeuzagallo.com/blog/react-native-bridge/

然後,在應用的實際執行過程中,兩個技術王國通過搭好的橋,彼此合作完成使用者功能:

(圖片來源:http://www.jianshu.com/p/978c4bd3a759)

因此,React Native的本質是在兩個技術王國之間搭建雙向橋樑,讓他們可以相互呼叫和響應。那麼就可以把上圖簡化一下:

React Native的效能瓶頸

經過上面的分析,我們就可以把一個React Native應用分成三個部分:Native王國、Bridge、JS王國。當應用執行時,Native王國和JS王國各自執行在自己獨立的執行緒中:

Native王國:

  • 執行在主執行緒上(可能會有些獨立的後臺執行緒處理運算,當前討論中可忽略)
  • iOS平臺上執行Object-C/Swift程式碼,Android平臺上執行Java/Kotlin程式碼
  • 負責處理UI的渲染,事件響應。

JS王國:

  • 執行在JS引擎的JS執行緒上
  • 執行JS程式碼
  • 負責處理業務邏輯,還包括了應該顯示哪個介面,以及如何給頁面加樣式。

在Native王國中,經過谷歌、蘋果公司多年的優化調整,Native程式碼能夠非常快速的執行在裝置上。在JS王國中,JS程式碼作為指令碼語言,也能夠很快速的執行在JS引擎上,這兩邊獨立來看都不會有效能問題。效能的瓶頸只會出現在從一個王國轉入另一個王國時,尤其是頻繁的在兩個王國之間切換時,兩個王國之間不能直接通訊,只能通過Bridge做序列化和反序列化,查詢模組,呼叫模組等各種邏輯,最終反應到應用上,就是UI層使用者可感知的卡頓。 因此,對React Native的效能控制就主要集中在如何儘量減少Bridge需要處理的邏輯上。

那麼,什麼情況下會需要Bridge處理邏輯呢?

  1. UI事件響應: 所有的UI事件都發生在Native側,會以事件的形式傳遞到JS側。這個過程非常簡單,也不會涉及大量的資料轉移。在React Native應用中,業務邏輯,應用狀態,資料都在JS側,所以UI事件只是一個觸發器,不會有效能問題。
  2. UI更新:前面已經說過JS負責決定應該展示哪個介面,以及如何樣式化介面,因此UI更新的發起方是JS側,更新時會向Native側同步大量的UI結構和資料,這類更新經常出現效能問題,尤其是在介面複雜、變動資料大,或者做動畫、變動頻繁時。
  3. UI事件響應和UI更新同時出現:在UI更新時,結構變化不大,則效能問題不大;但是如果這時又有UI事件觸發JS側邏輯處理,而該邏輯處理又比較複雜,耗時較長,導致JS側沒有時間片處理與Native側資料同步時,也會發生效能問題。

React Native的效能優化措施

前面已經解釋了React Native的效能瓶頸會在什麼地方,React Native官方也知道這些,其在React Native中提供了一些效能優化措施幫助開發者克服這些效能問題:

  1. 框架自帶的React基於Virtual Dom的Diff演算法保證了UI變動時傳遞的只是變化的UI部分,儘量減少需要同步的資料。
  2. 通過Direct Manipulation的方式直接在底層更新了Native元件的屬性,從而避免渲染元件結構和同步太多檢視變化所帶來的大量開銷。這樣的確會帶來一定的效能提升,同時也會使程式碼邏輯難以理清,而且並沒有解決從JS側到Native側的資料同步開銷問題。因此這個方式官方都不再推薦,更推薦的做法是合理使用setState()和shouldComponentUpdate()方法解決這類問題。
  3. 在遇到動畫效能問題時,可以使用Annimated類的庫,一次性把如何變化的宣告傳送到Native側,Native側根據接收到的宣告自己負責接下來的UI更新。不需要每幀的UI變化都同步一次資料。
  4. Native和JS混編,把會大量變化的元件做成Native元件,這樣UI的變更資料直接在Native側自己處理了,無需通過Bridge,而不變的內部元件因為沒有資料更新需要同步,所以也不會使用到Bridge。框架提供的NavigatorIOS相對於Navigator的效能提升就是這種做法。
  5. 遇到事件響應和UI更新同時發生導致的效能問題時,可以使用Interaction Manager把那些耗時較長的工作安排到所有互動或動畫完成之後再進行。

探求效能和效率平衡的套路

在瞭解了React Native的效能瓶頸和優化措施之後,就可以大概總結一個探尋React Native開發效率和效能平衡點的套路:

第一步: 全JS實現, 從一開始在技術選型上用React Native就是為了保證開發的效率,在沒有遇到效能問題之前,最大化效率是團隊的一致追求。

第二步: 從JS側進行效能優化

  • 對於那些明顯會涉及Bridge、需大量處理邏輯的場景,比方說動畫,複雜的手勢操作響應等,嘗試使用經過優化過的庫(比方說:Animated),一次傳遞動畫或者資料整個資料的描述給Native,Native側自己會按照宣告執行下去。
  • 使用InteractionManager把耗時操作遞延到UI響應之後,處理那些存在因為耗時的JS操作導致的UI響應效能問題。

第三步:在真機上實測,檢查效能問題點。不要過早優化,找到問題點再一一處理。

第四步:如果經過JS端的優化策略之後,在裝置上還是有效能問題,可以把有問題的部分以Native方式實現,這也是為什麼要推薦React Native團隊中有10%左右的Native Developer。在這個步驟中,需要注意問題的隔離方式,假設一個場景:在移動一個Container時,Container的UI同時發生變化,但是Container內部的內容並沒有發生變化,這種情況下,只需要用Native實現Container,Container內部的元件還是以JS實現。

相關文章