構建高性能 React Native 跨端應用—引擎與渲染

一 前言

React Native 目前是一個非常成熟的跨端技術方案, 總體來看 RN 在 Native 端的表現也是非常出色的,即便是這樣,在 RN 構建的應用中,性能也是不容忽略的一部分,尤其在移動端應用中,受到內存的影響很大,如果用法不當,很容易造成閃退,崩潰的情況發生。

所以,本章節我們來淺談一下在 RN 中的性能優化,以及如何構建高性能的 RN 應用。

我這裏把性能優化的方向,分成引擎層面渲染層面圖像層面內存層面 等四個層面,本文將重點介紹前兩個。

二 引擎層面

在 React 跨端動態化序章—從 JS 引擎到 RN 落地 中,我們講到 React Native 分爲 JS 層,C++ 層和 Native 層三部分構成,在 JS 層,JS 是由 JSC 作爲引擎驅動 (目前已經開始主推 Hermes )了。所以想要快速打開一個 RN 應用,創建 JS 引擎是一個關鍵的環節。

以安卓側爲例子,RN 應用的啓動流程如下:

1.jpg

在上述流程中,JS 引擎的構建,解析並運行 JS Bundle,準備 JS 上下文是最佔用時間的一部分。所以對於 JS 引擎的預加載就顯得非常重要。

預加載技術

引擎預加載和業務場景息息相關,對於一些上下游的頁面會有一定的要求,在加載當前頁面的時候,如果下游頁面是 RN 頁面,那麼會進行引擎的預加載,構建初始化的 JS 環境。比如如下的頁面棧:

2.jpg

如上一個業務線上存在 A,B,C 三個頁面,其中 C 是 RN 頁面,那麼當從 A 進入到 B 的時候,開始啓動預加載,加載 C 頁面的 Bundle,這樣進入到 C 頁面後,就不需要做初始化 JS 運行環境等操作,大幅度提高了頁面的秒開率

但是預加載的 JS 引擎不能一直存在,所以可以在 從 B -> A 的時候,回收引擎。還有一點需要注意的是,預加載的引擎需要在內存中保留一段時間後纔會被回收,所以在進入一個頁面中的時候,不要預加載很多頁面,這樣就會造成內存瞬間暴漲,容易引起 APP 閃退。

可以說預加載是對下一個頁面預處理,那麼對於引擎優化層面上,還有一個優化技巧那就是引擎複用。

引擎複用技術

引擎複用,也是一種對頁面初始化加載的優化手段,比如 A 進入 RN 的 B 頁面,當 B 離開回到 A 的時候,B 的引擎並沒有直接回收,而是被保存下來,當 A 再次進入到 B 的時候,直接服用引擎,這樣當第二次進入 B 的時候,打開的速度非常快。

62401666492036_.pic.jpg

引擎複用是一種短時間內對引擎的保活,但是並不意味着引擎就可以一直存在,如果一直存在,還會面臨內存喫緊的情況,長此以往就會讓應用內存越來越大,導致崩潰。所以需要對引擎進行短時間的保活,一般都會存在幾分鐘。

引擎複用比較適合從列表頁到詳情頁的場景,比如從商品列表到商品詳情,用戶可能多次從商品詳情返回列表,然後再次進入商品詳情。

引擎複用有一個弊端需要開發者注意,因爲引擎的存在,會讓 JS 中的一些全局變量(比如 Redux 中的狀態)無法被垃圾回收,在下一次複用的時候,會影響到新的 RN 應用的數據狀態,一個靠譜的方案就是,在 RN 應用在初始化的時候清除數據。

三 渲染層面

如上講到了 React Native 在 JS 引擎方面上的優化,主要的影響就是頁面打開的時間,白屏時間,以及秒開率,接下來我們分析一下在 React Native 運行時的優化手段。

組件渲染也是很重要的一部分,因爲在 React Native 中,渲染成本比 web 端更大,爲什麼這麼說呢?我們先來看簡單分析一下 React web 應用和 React Native 應用的渲染區別。

在 React web 應用中渲染流程是,先由 element 對象轉換成虛擬 DOM fiber 對象,再有 fiber 轉換成真實 DOM ,最後交給瀏覽器去繪製。

但是在 RN 中,渲染流程會更加複雜,在構建 fiber 對象後,需要通過橋的方式通知 UI Manage 構建一顆 Shadow Tree,Shadow Tree 可以理解爲是 "Virtual DOM" 在 Native 的映射,擁有和 Virtual DOM 相同的樹形層級關係。最後 Native 根據 Shadow Tree 映射成 Native 元素並渲染。

React Native 渲染過程中需要三個線程共同完成。

所以在 RN 端,頁面的渲染成本會更高,這就要求開發者在開發過程中,需要監控一下組件的渲染次數,可以通過 React 層面去減少頁面或者組件的 rerender。

減少 rerender 的次數

在 RN 中,減少頁面渲染方案和瀏覽器端是統一的,本質上都是在 React render 階段的優化手段。

我們來回顧一下 React 控制渲染的策略:

1 緩存 React.element 對象

第一種是對 React.element 對象的緩存。這是一種父對子的渲染控制方案,緩存了 element 對象。這種方案在 React Native 中同樣受用。

import React from "react"
import { View, TouchableOpacity, Text } from "react-native"

function Children () {
    return <View>子組件</View>
}

function App(){
    const [ number, setNumber ] = React.useState(0)
    /* 這裏把 Children 組件對應的 element 元素緩存起來了 */
    const children = React.useMemo(()=><Children />,[])
    const onPress = () => setNumber(number => number + 1);
    return <View >
        父組件
        <TouchableOpacity onPress={onPress} >
           <View>
              <Text>add</Text>
           </View>
        </TouchableOpacity>
    </View>
}

如上當點擊 add 按鈕的時候,App 會重現渲染,但是由於 Children 組件對應的 element 被緩存起來了,所以並不會跟隨着父組件渲染。一定程度上優化了性能。

2 PureComponent

純組件是一種發自組件本身的渲染優化策略,當開發類組件選擇了繼承 PureComponent ,就意味這要遵循其渲染規則。規則就是淺比較 state 和 props 是否相等。

import React from "react"
import { View, TouchableOpacity, Text } from "react-native"

class Children extends React.PureComponent{
    //...
}

3 shouldComponentUpdate

有的時候,把控制渲染,性能調優交給 React 組件本身處理顯然是靠不住的,React 需要提供給使用者一種更靈活配置的自定義渲染方案,使用者可以自己決定是否更新當前組件,shouldComponentUpdate 就能達到這種效果。

 shouldComponentUpdate(newProp,newState,newContext){
    if(newProp.propsNumA !== this.props.propsNumA || newState.stateNumA !== this.state.stateNumA ){
        return true /* 只有當 props 中 propsNumA 和 state 中 stateNumA 變化時,更新組件  */
    }
    return false 
}

4 React.memo

React.memo(Component,compare)

React.memo 可作爲一種容器化的控制渲染方案,可以對比 props 變化,來決定是否渲染組件,首先先來看一下 memo 的基本用法。React.memo 接受兩個參數,第一個參數 Component 原始組件本身,第二個參數 compare 是一個函數,可以根據一次更新中 props 是否相同決定原始組件是否重新渲染。

memo 的幾個特點是:

React.memo: 第二個參數 返回 true 組件不渲染 , 返回 false 組件重新渲染。和 shouldComponentUpdate 相反,shouldComponentUpdate : 返回 true 組件渲染 , 返回 false 組件不渲染。 memo 當二個參數 compare 不存在時,會用淺比較原則處理 props ,相當於僅比較 props 版本的 pureComponent 。

RN 開發者可以通過如上的四種方式減少組件的渲染次數,進而優化性能。

渲染分片

運行 RN 的宿主環境,基本都是移動端,在移動端,有內存大的高端手機,也有內存小的低端手機,在內存小的低端手機上,如果在初始化階段一次性加載大量的模塊,比如初始化加載大量的圖片模塊組件,就會讓內存端時間內暴漲,低端的手機本來內存就小,就會達到內存的閥值,就會造成 App 崩潰。RN 應用本身就比較耗內存,即便有 LRU 算法,可以處理長時間內的增量內存,但是內存的處理,還是需要時間去消化,那麼短時間內內存暴漲依舊是一個非常頭疼的問題。

還有一點就是上面說到,渲染本身也耗性能,如果短時間內加載大量的模塊,就會讓加載時間過長,從而讓用戶等到響應的時間變長。

爲了解決上面的兩點問題,渲染分片就顯得格外重要了,可以根據業務場景,渲染模塊按需加載,而不是一次性渲染大量的模塊,首先就要對模塊定義渲染的優先級,重要的模塊優先渲染,次要的模塊滯後渲染。

就像當用戶進入一個商品詳情頁,最優先展示的應該用是有關該商品的信息,比如圖片,價格,生產地等等,而一些不重要的模塊,比如推薦其他商品,就不需要優先渲染。

那麼比如有三個組件 A,B,C 我們就可以在渲染 A 之後再渲染 B ,渲染 B 之後再渲染 C。

那麼首先有一個問題就是如何知道組件渲染完成了? 還好即便是 RN 也保持了 React 的活性。可以用對應的生命週期或者是 hooks ,來感知組件更新完畢。

在類組件中,可以通過 componentDidMount 知道組件初始化渲染完成,同樣在函數組件中,可以通過 useEffect 或者 useLayoutEffect 來達到相同的目的。

比如實現上面 A -> B -> C 的渲染流程,可以在 A 的 componentDidMount / useEffect 來渲染 B ,然後 B 渲染完成以同樣的手段再渲染 C。甚至可以通過 setTimeout 來加一個短暫的延時。這樣的操作就像給渲染加了調度,去控制每一個模塊的渲染順序,當然渲染調度寫在每一個業務代碼中,不是很友好,所以爲了解決這個問題,可以寫一個 HOC 來包裹業務組件,通過 HOC 組件生命週期來控制業務組件的加載時機,達到渲染分片的目的。

具體的代碼實現可以參考 「React 進階」 學好這些 React 設計模式,能讓你的 React 項目飛起來 中 HOC 的介紹場景。

長列表優化

長列表是移動端應用中,一個比較常見的場景,基本上主流的 App 應用中,都有長列表的影子。

在 Native 中,對於長列表本來就有比較成熟的方案,在 Native 應用中,對於每個列表 item 可以進行復用,在 RN 中,也提供了對應的組件來處理長列表的情況,比如說 FlatList 和 SectionList。

FlatList 是高性能的簡單列表組件,SectionList 是高性能的分組 (section) 列表組件。官方網站中介紹比較詳細,這裏就不多說了

四 總結

本文從引擎與渲染兩個方面介紹了 RN 優化手段,希望這篇文章的能給 React Native 開發同學一個性能優化上啓發。

參考

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/MVXde22OtRqp-evBDzFyfA