該文章翻譯自Facebook官方部落格,傳送門
React Native 允許我們運用 React 和 Relay 提供的宣告式的程式設計模型,寫JavaScript來構建我們的 iOS 和 Android 的應用。這樣的做法使得我們的程式碼更精簡,更容易理解和閱讀,這些程式碼還可以在多個平臺共享。我們也可以加快迭代速度(因為在開發時不用等待漫長的編譯。使用React Native,我們可以釋出更快,打磨更多細節,讓應用執行的更流暢。這其中優化效能是我們工作的一大重要部分,接下來講述 Facebook 如何使應用效能足足提升兩倍的故事~
為什麼要加快?
當應用執行的更快,內容載入的更迅速,就意味著使用者可以有更多時間來使用應用,流暢的動畫讓使用者更加享受的使用應用。在新型市場中,2G網路和幾年前的機型還是主力。這時那些效能良好的和那些執行卡頓就有很大差別了。
自從釋出了 iOS 和 Android 版本的 React Native 後,我們團隊一直在諸如 提升列表檢視的滾動效能,優化記憶體佔有,讓 UI 介面更具響應性和加快應用啟動速度 上做了不少工作。這其中應用啟動關乎初次印象和是框架其他部分的壓力源頭,所以它是要解決的頭等難題。
量化一切
我們把Facebook的iOS版中的事件主頁用RN重新實現(在更多標籤頁下點選事件進入檢視)。這是個非常好的用於測試效能的例子,因為原生版已經做了大量的優化工作,而且該頁面也是非常好的典型列表互動的例子。
接下來,我們自動化的 CT-Scan 效能測試來幫助我們自動定位到我們需要到的標籤頁。然後反覆開啟和關閉事件主頁50次。在每次互動中,我們能夠記錄下從點選事件按鈕到事件主頁能夠被完整顯示的時間,我們也新增更多詳細的效能埋點來告訴我們啟動過程哪些步驟是緩慢或消耗CPU的
下面是我們記錄和測量的一些步驟的大致描述:
1 原生啟動:初始化JavaScript虛擬機器和其他一些原生模組(如磁碟快取,網路,UI管理器等)
2 JS初始化和依賴載入:從手機儲存中讀取被壓縮的JS程式碼,載入到JavaScript虛擬機器,從而解析和產生位元組碼,載入相關的依賴
3 取資料前:載入和執行事件主頁的應用程式碼,構建Relay的查詢語句,然後觸發取資料。
4 取資料:從手機磁碟快取讀取資料
5 JS渲染:初始化所有相關的React元件,把它們傳送到原生的UI管理器模組來顯示。
6 原生渲染:在shadow執行緒中先通過根據 FlexBox 佈局計算檢視大小。然後在主執行緒中建立和定位這些檢視。
我們根據於此的黃金法則是:永遠不要忘了迴歸測試。我們持續的執行它來追蹤效能提升和功能迴歸。開發者在提交改動的程式碼之前用它對特定的提交做執行和詳細的效能分析。其他的一些測試也需要被同樣的方式建立來衡量諸如功能效能和記憶體使用等
啟動時發生了什麼
當我們設定好自動效能追蹤,我們需要一個工具來給我們更多細節來決定啟動過程中的那些部分需要優化。我們在我們框架裡新增詳細的啟動/暫停的效能錨點,收集資料,使用 catapult 檢視器來定位熱點和阻塞執行緒間互動的。也可以從開發者選單下觸發開始對我們應用的效能分析。
在RN中,程式碼是在JavaScript執行緒中執行的。每次你要寫資料到磁碟,在一次網路請求,或者取一些其他原生的資源(如攝像機),你的程式碼都需要呼叫原生模組。當你要渲染力你的 React 元件,它們會被轉發到介面管理器的原生模組中,它在主執行緒中來執行佈局和建立相應的檢視。橋協議來轉發請求到原生模組和被回撥到你的JS程式碼(如果需要)。在RN中,所有原生的呼叫必須是非同步的來避免阻塞主執行緒和JS執行緒。
在下面的事件元件的啟動視覺化圖中,我們可以看到應用在 JS 佇列中執行,為了顯示事件列表,觸發了相關的快取讀取(在本地儲存佇列中被非同步觸發)。一旦它取得了快取資料,應用在 JS 佇列用 React 渲染事件單元格,接著又傳給柵格佇列來佈局和最終傳給主佇列來建立檢視顯示。這個例子展示了多個快取讀取(組合成單個常用讀取操作可以做到更快)和一些React在JS執行緒上的渲染操作可以被統一合併。
效能提升
下面是那些我們在實施過程中最重要的效能和時序安排的提升做法,同時配有對應程式碼提交的連結。
啟動時少做些
- 清理 Require/Babel 輔助方法(高優先順序):清理掉那些在require時執行的多餘邏輯和程式碼,那些事為了網站準備而不是 RN
- 避免在載入打包檔案時,複製和解碼字串
- 去除開發時才需要的模組:不像編譯程式碼,在釋出模式下 JS 不需要用來去除debug特性的前處理器。使用 Babel 的轉換函式,我們可以剔除那些在 DEV 語句下方的程式碼,來有效的減少要打包的程式碼量,從而節省 JavaScript 解析時間。
- 在伺服器端生成事件描述
安排合適時機執行
- 懶載入
- Relay的增量快取讀取:Relay一開始是web專案而生所以僅僅把請求響應放在記憶體中 – 要從磁碟讀取的第一個請求的快取響應需要從磁碟中讀取全部的快取到記憶體中。通過只讀取滿足特定查詢請求的快取,我們可以顯著減少 I/O 負載和原生到JS橋的流量。
- 不用批量橋協議呼叫,要批量Relay呼叫:一開始我們認為通過把JS請求批量傳送給原生模組可以減少呼叫原生到JS橋的負載,但是效能分析告訴我們JS和原生間的橋呼叫根本不是效能瓶頸。事實上,UI介面或快取讀取的批量操作的延遲也會延遲原生執行緒的操作,從而影響應用效能。在其他case上,注入Relay用於拉取多個鍵值資料的快取讀取,通過批處理可以有顯著提升。
- 更早的介面填充
- 懶載入原生模組
- 對文字元件的觸控做懶繫結:繫結觸控事件回撥會需要不少時間。所以我們現在僅僅先繫結觸控開始事件touch down event(就是當你第一次觸控物件時)然後只當你觸控物件後才開始繫結其他回撥函式,而不是一開始就全部繫結對調
- 延遲流行事件的查詢:
為光速做準備
幾個月前,事件主頁的啟動在 iPhone5 上需要2秒。經過我們在RN上的大量效能優化工作,在倫敦,門洛帕克和紐約的RN,React和Relay團隊,事件主頁的啟動被加快了一倍。而且大部分我們實施的優化是在RN的框架層的,這就意味著開發者們的RN應用也會自動得益於這些工作(當他們把應用遷移到最新版本的RN下
這些優化才僅僅是個開始:我們會繼續在整個棧的各個部分都開展工作,從JavaScript程式碼解析時間到資料拉取效能。同時,你們也可以給社群貢獻,學習如何讓應用更快,在社群論壇提出你可能遇到的任何問題。