React Native 性能優化指南

2020 年談 React Native,在突飛猛進的前端圈,可能算比較另類了。文章動筆以前我也猶豫過,可是想到寫技術文章又不是趕時髦,啥新潮寫啥,因此仍是動筆寫了這篇 React Native 性能優化的文章。javascript

本文談到的 React Native 性能優化,還沒到修改 React Native 源碼那種地步,因此通用性很強,對大部分 RN 開發者來講都用得着。html

本文的內容,一部分是 React/RN/Android/iOS 官方推薦的優化建議,一部分是啃源碼發現的優化點,還有一部分是能夠解決一些性能瓶頸的優秀的開源框架。**本文總結的內容你不多在網絡上看到,因此看完後必定會有所收穫。**若是以爲寫的不錯,請不要吝嗇你的贊,把這篇 1w 多字的文章分享出去,讓更多的人看到。前端

看文章前要明確一點,一些優化建議並非對全部團隊都適用的。有的團隊把 React Native 當加強版網頁使用,有的團隊用 React Native 實現非核心功能,有的團隊把 React Native 當核心架構,不一樣的定位須要不一樣的選型。對於這些場景,我在文中也會提一下,具體使用還須要各位開發者定奪。java

<br/>react

目錄:

  • 1、減小 re-render
  • 2、減輕渲染壓力
  • 3、圖片優化那些事
  • 4、對象建立調用分離
  • 5、動畫性能優化
  • 6、長列表性能優化
  • 7、React Native 性能優化用到的工具
  • 8、推薦閱讀

<br/>android

1、減小 re-render

由於 React Native 也是 React 生態系統的一份子,因此不少 React 的優化技巧能夠用到這裏,因此文章剛開始先從你們最熟悉的地方開始。webpack

對於 React 來講,減小 re-render 能夠說是收益最高的事情了。git

1️⃣ shouldComponentUpdate

📄 文檔https://react.docschina.org/docs/optimizing-performance.html#shouldcomponentupdate-in-actionsxgithub

簡單式例:web

class Button extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    if (this.props.color !== nextProps.color) {
      return true;
    }
    return false;
  }

  render() {
    return <button color={this.props.color} />;
  }
}

不管哪篇文章,談到 React 性能優化,shouldComponentUpdate 必定是座上賓。

咱們經過這個 API,能夠拿到先後狀態的 state/props,而後手動檢查狀態是否發生了變動,再根據變動狀況來決定組件是否須要從新渲染。

🔗 官方文檔shouldComponentUpdate 的做用原理和使用場景已經說的很是清晰了,我就沒有必要搬運文章了。在實際項目中,閱文集團的 🔗 React Native 應用**「元氣閱讀」**也作了很好的示範,🔗 Twitter 的性能優化分享也作的圖文並茂,可有很高的參考價值,對此感興趣的同窗能夠點擊跳轉查看。

在此我想提醒的是,shouldComponentUpdate 是強業務邏輯相關的,若是使用這個 API,你必須考慮和此組件相關的全部 props 和 state,若是有遺漏,就有可能出現數據和視圖不統一的狀況。因此使用的時候必定很是當心。

在此我想提醒的是,shouldComponentUpdate 是強業務邏輯相關的,若是使用這個 API,你必須考慮和此組件相關的全部 props 和 state,若是有遺漏,就有可能出現數據和視圖不統一的狀況。因此使用的時候必定很是當心。

2️⃣ React.memo

**📄 文檔:**https://react.docschina.org/docs/react-api.html#reactmemo

React.memo 是 React v16.6 中引入的新功能,是一個專門針對 React 函數組件的高階組件。

默認狀況下,它和 PureComponent 同樣,都是進行淺比較,由於就是個高階組件,在原有的組件上套一層就能夠了:

const MemoButton = React.memo(function Button(props) {
  return <button color={this.props.color} />;
});

若是想和 shouldComponentUpdate 同樣,自定義比較過程,React.memo 還支持傳入自定義比較函數:

function Button(props) {
  return <button color={this.props.color} />;
}
function areEqual(prevProps, nextProps) {
  if (prevProps.color !== nextProps.color) {
      return false;
    }
  return true;
}
export default React.memo(MyComponent, areEqual);

值得注意的是areEqual() 這個函數的返回值和 shouldComponentUpdate 正好相反,若是 props 相等,areEqual() 返回的是 trueshouldComponentUpdate 卻返回的是 false

3️⃣ React.PureComponent

**📄 文檔:**https://react.docschina.org/docs/react-api.html#reactpurecomponent

簡單式例:

class PureComponentButton extends React.PureComponent {
  render() {
    return <button color={this.props.color} />;
  }
}

shouldComponentUpdate 相對應,React 還有一個相似的組件 React.PureComponent,在組件更新前對 props 和 state 作一次淺比較。因此涉及數據嵌套層級過多時,好比說你 props 傳入了一個兩層嵌套的 Object,這時候 shouldComponentUpdate 就很爲難了:我究竟是更新呢仍是不更新呢?

考慮到上面的狀況,我在項目中通常不多用 PureComponent雖然很簡單易用,可是面對複雜邏輯時,反而不如利用 shouldComponentUpdate 手動管理簡單粗暴。固然這個只是我的的開發習慣,社區上也有其餘的解決方案:

  • 把組件細分爲很小的子組件,而後統一用 PureComponent 進行渲染時機的管理
  • 使用 immutable 對象,再配合 PureComponent 進行數據比較(🔗 參考連接:有贊 React 優化
  • ......

在這個問題上仁者見仁智者見智,在不影響功能的前提下,主要是看團隊選型,只要提早約定好,其實在平常開發中工做量都是差很少的(畢竟不是每一個頁面都有必要進行性能優化)。

<br/>

2、減輕渲染壓力

React Native 的佈局系統底層依賴的是 🔗 Yoga 這個跨平臺佈局庫,將虛擬 DOM 映射到原生布局節點的。在 Web 開發中,99% 的狀況下都是一個 Virtual DOM 對應一個真實 DOM 的,那麼在 React Native 中也是一一對應的關係嗎?咱們寫個簡單的例子來探索一下。

咱們先用 JSX 寫兩個橙色底的卡片,除了卡片文字,第一個卡片還嵌套一個黃色 View,第二個卡片嵌套一個空 View:

// 如下示例 code 只保留了核心結構和樣式,領會精神便可
render() {
  return (
    <View>
      <View style={{backgroundColor: 'orange'}}>
        <View style={{backgroundColor: 'yellow'}}>
          <Text>Card2</Text>
        </View>
      </View>
      <View style={{backgroundColor: 'orange'}}>
        <View>
          <Text>Card2</Text>
        </View>
      </View>
    </View>
  );
};

react-devtools 查看 React 嵌套層級時以下所示:

從上圖中能夠看出,React 組件和代碼寫的結構仍是一一對應的。

咱們再看看 React Native 渲染到原生視圖後的嵌套層級(iOS 用 Debug View Hierarchay,Android 用 Layout Inspector):

從上圖能夠看出,iOS 是一個 React 節點對應一個原生 View 節點的;Android 第二個卡片的空白 View 卻不見了!

若是咱們翻一翻 React Native 的源碼,就會發現 React Native Android UI 佈局前,會對只有佈局屬性的 View(LAYOUT_ONLY_PROPS 源碼)進行過濾,這樣能夠減小 View 節點和嵌套,對碎片化的 Android 更加友好。

經過這個小小的例子咱們能夠看出,React 組件映射到原生 View 時,並非一一對應的,咱們瞭解了這些知識後,能夠如何優化佈局呢?

1️⃣ 使用 React.Fragment 避免多層嵌套

📄 React Fragments 文檔https://zh-hans.reactjs.org/docs/fragments.html

咱們先從最熟悉的地方講起——React.Fragment。這個 API 可讓一個 React 組件返回多個節點,使用起來很簡單:

render() {
  return (
    <React.Fragment>
      <ChildA />
      <ChildB />
      <ChildC />
    </React.Fragment>
  );
}

// 或者使用 Fragment 短語法
render() {
  return (
    <>
      <ChildA />
      <ChildB />
      <ChildC />
    </>
  );
}

Fragments 做用仍是蠻明顯的:避免你多寫一層 View。用處仍是很廣的,好比說本身業務上封裝的 React 組件,React Native 官方封裝的組件(好比說 ScrollView or Touchable* 組件 ),活用這個屬性,能夠減小你的 View 嵌套層級。

2️⃣ 減小 GPU 過分繪製

咱們在業務開發時,常常會遇到這種場景:整個界面的背景色是白色的,上面又加了一個白色背景的卡片組件,卡片內部又包含了一個白色背景的小組件......

// 如下示例 code 只保留了核心結構和樣式,領會精神便可
render() {
  return (
    <View>
      <View style={{backgroundColor: 'white'}}>
        <View style={{backgroundColor: 'white'}}>
          <Text style={{backgroundColor: 'white'}}>Card1</Text>
        </View>
      </View>
      <View>
        <View>
          <Text>Card2</Text>
        </View>
      </View>
    </View>
  );
};

首先咱們要明確一點,屏幕上的每一個像素點的顏色,是由多個圖層的顏色決定的,GPU 會渲染這些圖層混合後的最終顏色,可是,iOS 和 Android 的 GPU 渲染機制是不一致的。

雖然上面的代碼最後的的渲染結果在顯示上都是白色的,可是 GPU 的優化是不同的。咱們用 iOS 的 Color Blended Layers 和 Android 的🔗 GPU 過分繪製調試工具查看最後的渲染結果:

對於 iOS 來講,出現紅色區域,就說明出現了顏色混合:

  • Card1 的幾個 View 都設置了非透明背景色,GPU 獲取到頂層的顏色後,就再也不計算下層的顏色了
  • Card2 的 Text View 背景色是透明的,因此 GPU 還要獲取下一層的顏色進行混合

對於 Android 來講,GPU 會畫蛇添足地渲染對用戶不可見的像素。有一個顏色指示條:白 -> 藍 -> 綠 -> 粉 -> 紅,顏色越日後表示過分繪製越嚴重。

  • Card1 的幾個 View 都設置了非透明背景色,紅色表示起碼發生了 4 次過分繪製
  • Card2 只有文字發生了過分繪製

在過渡繪製這個測試上,iOS 和 Android 的實驗結果幾乎是徹底相反的,因此解決方案確定不是一箭雙鵰的,我我的認爲,React Native 開發作視圖優化時,應該優先優化 Android,因此咱們能夠從如下幾點優化:

  • 減小背景色的重複設置:每一個 View 都設置背景色的話,在 Android 上會形成很是嚴重的過分繪製;而且只有佈局屬性時,React Native 還會減小 Android 的佈局嵌套
  • 避免設置半透明顏色:半透明色區域 iOS Android 都會引發過分繪製
  • 避免設置圓角:圓角部位 iOS Android 都會引發過分繪製
  • 避免設置陰影:陰影區域 iOS Android 都會引發過分繪製
  • ......

避免 GPU 過分繪製的細節太多了,通常頁面不須要這種精細化管理,長列表優化時能夠考慮一下這個方向。

<br/>

3、圖片優化那些事

性能優化的另外一個大頭就是圖片。這裏的圖片優化不只僅指減小圖片大小,減小 HTTP 帶寬佔用,我會更多的討論一些 Image 組件上的優化,好比說緩存控制,圖片採樣等技術。

1️⃣ Image 組件的優化項

React Native 的 Image 圖片組件,若是隻是做爲普通的圖片展現組件,那它該有的都有了,好比說:

  • 加載本地/網絡圖片
  • 自動匹配 @2x/@3x 圖片
  • 圖片加載事件:onLoadStart/onLoad/onLoadEnd/onError
  • loading 默認圖 or loading 指示器
  • ......

可是,若是你要把它當一個圖片下載管理庫用時,就會很是的難受,由於 Image 的這幾個屬性在 iOS/Android 上有不一樣的表現,有的實現了有的沒有實現,用起來很是不順手。

在講解圖片優化前,咱們先想一下,一個基本的圖片下載管理庫要實現什麼:

  1. 圖片類型:首先你的主要職責是加載圖片,你起碼能加載多種圖片類型
  2. 下載管理:在加載多張圖片的場景,能管理好多個請求,能夠控制圖片加載的優先級
  3. 緩存管理:作好三級緩存,不能每一個圖片都要請求網絡,均衡好內存緩存和磁盤緩存的策略
  4. 多圖加載:大量圖片同時渲染時,如何讓圖片迅速加載,減小卡頓

針對上面的 4 條原則,咱們來一一刨析 Image 組件。

1.圖片類型

基礎的 png/jpg/base64/gif 格式,支持良好。不過要注意的是,想要 Android 加載的 gif 圖片動起來,要在 build.gradle 裏面加一些依賴,具體內容能夠看這個 🔗 連接

若是要加載 webp 格式的圖片,就有些問題了。做爲 Google 推出的一種圖片格式,Android 天然是支持的,可是 iOS 就不支持了,須要咱們安裝一些第三方插件。

2.下載管理

先說結論,Image 組件對圖片的下載管理能力基本爲 0。

Image基本上只能監聽單張圖片的加載流程:onLoadStart/onLoad/onLoadEnd/onError,若是要控制多張圖片的下載優先級,對不起,沒有。

3.緩存管理

緩存這裏要從兩方面說,一是經過 HTTP 頭信息管理緩存,二是直接經過一些組件屬性管理緩存。

Image 組件請求網絡圖片時,實際上是能夠加 HTTP header 頭信息的,這樣就能夠利用 HTTP 緩存來管理圖片,寫法以下面代碼所示:

<Image
  source={{
    uri: 'https://facebook.github.io/react/logo-og.png',
    method: 'POST',
    headers: {
      Pragma: 'no-cache',
    },
    body: 'Your Body goes here',
  }}
  style={{width: 400, height: 400}}
/>

具體的控制參數能夠參考 🔗 MDN HTTP 緩存,這裏就不細說了。

直接經過屬性控制圖片緩存,iOS 有。Android?對不起,沒有。

iOS 能夠經過 source 參數裏的 cache 字段控制緩存,屬性也是很是常見的那幾種:默認/不使用緩存/強緩存/只使用緩存。具體的使用能夠看 🔗 iOS Image 緩存文檔

4.多圖加載

都快到 5G 時代了,短視頻/VLog 你們都每天刷了,更不用說多圖場景了,基本上已是互聯網應用的標配了。

講圖片加載前先明確一個概念:圖片文件大小 != 圖片加載到內存後的大小

咱們常說的 jpg png webp,都是原圖壓縮後的文件,利於磁盤存儲和網絡傳播,可是在屏幕上展現出來時,就要恢復爲原始尺寸了。

React Native 性能優化——圖片內存優化

好比說一張 100x100 的 jpg 圖片,可能磁盤空間就幾 kb,不考慮分辨率等問題,加載到內存裏,就要佔用 3.66 Mb。

// 不一樣的分辨率/文件夾/編碼格式,都會帶來數值差別
// 下面的計算只是最通常的場景,領會精神便可

(100 * 100 * 3) / (8 * 1024) = 3.66 Mb
(長 * 寬 * 每一個像素佔用字節數) / (8 * 1024) = 3.66 Mb

上面只是 100x100 的圖片,若是圖片尺寸增長一倍,圖片在內存裏的大小是按平方倍數增加的,數量一多後,內存佔用仍是很恐怖的。

在多圖加載的場景裏,通過實踐,iOS 無論怎麼折騰,表現都比較好,可是 Android 就容易出幺蛾子。下面咱們就詳細說說 Android 端如何優化圖片。

在一些場景裏,Android 會內存爆漲,幀率直接降爲個位數。這種場景每每是小尺寸 Image 容器加載了特別大的圖片,好比說 100x100 的容器加載 1000x1000 的圖片,內存爆炸的緣由就是上面說的緣由。

那麼這種問題怎麼解決呢?Image 有個 resizeMethod 屬性,就是解決 Android 圖片內存暴漲的問題。當圖片實際尺寸和容器樣式尺寸不一致時,決定以怎樣的策略來調整圖片的尺寸。

  • resize小容器加載大圖的場景就應該用這個屬性。原理是在圖片解碼以前,會用算法對其在內存中的數據進行修改,通常圖片大小大概會縮減爲原圖的 1/8。
  • scale:不改變圖片字節大小,經過縮放來修改圖片寬高。由於有硬件加速,因此加載速度會更快一些。
  • auto:文檔上說是經過啓發式算法自動切換 resize 和 scale 屬性。這個啓發式算法很是誤導人,第一眼看上去還覺得是會對比容器尺寸和圖片尺寸採用不一樣策略。但我看了一下源碼,它只是單純的判斷圖片路徑,若是是本地圖片,就會用 resize,其餘都是 scale 屬性,因此 http 圖片都是 scale 的,咱們還得根據具體場景手動控制。

順便提一下,Android 圖片加載的時候,還會有一個 easy-in 的 300ms 加載動畫效果,看上去會以爲圖片加載變慢了,咱們能夠經過設置 fadeDuration 屬性爲 0,來關閉這個加載動畫。

2️⃣ 優先使用 32 位色彩深度的圖片

📄 色彩深度 wikihttps://github.com/DylanVann/react-native-fast-image/blob/master/README.md

色彩深度這個概念其實前面也提了一下,好比說咱們經常使用的帶透明度 PNG 圖片,就是 32 位的:

  • R:紅色,佔據 8 bit
  • G:綠色,佔據 8 bit
  • B:藍色,佔據 8 bit
  • A:透明通道,佔據 8 bit

爲啥推薦使用 32 bit 圖片呢?直接緣由有 2 個:

  1. Android 推薦使用 🔗 ARGB_8888 格式的圖片,由於這種圖片顯示效果更好
  2. iOS GPU 只支持加載 32 bit 的圖片。若是是其餘格式的(好比說 24 bit 的 jpg),會先在 CPU 裏轉爲 32 bit,再傳給 GPU

<br/>

雖然推薦 32 bit 圖片,可是說實話,這個對前端開發是不可控的,由於圖片來源通常就 2 個:

  1. 設計師的切圖,由設計師控制
  2. 網絡上的圖片,由上傳者控制

因此想針對這一點進行優化的話,溝通成本挺高,收益反而不高(通常只在長列表有些問題),但也是圖片優化的一個思路,故放在這一節裏。

3️⃣ Image 和 ImageView 長寬保持一致

前面舉了一個 100x100 的 ImageView 加載 1000x1000 Image 致使 Android 內存 OOM 的問題,咱們提出了設置 resizeMethod={'resize'} 的方法來縮減圖片在內存中的體積。其實這是一種無奈之舉,若是能夠控制加載圖片的大小,咱們應該保持 Image 和 ImageView 長寬一致。

首先咱們看看長寬不一致會引發的問題:

  • Image 小於 ImageView:圖片不清晰,表情包電子包漿質感
  • Image 大於 ImageView:浪費內存,有可能會引發 OOM
  • 尺寸不一致會帶來抗鋸齒計算,增長了圖形處理負擔

React Native 開發時,佈局使用的單位是 pt,和 px 存在一個倍數關係。在加載網絡圖片時,咱們可使用 React Native 的 🔗 PixelRatio.getPixelSizeForLayoutSize 方法,根據不一樣的分辨率加載不一樣尺寸的圖片,保證 Image 和 ImageView 長寬一致。

4️⃣ 使用 react-native-fast-image

📄 react-native-fast-image 文檔https://github.com/DylanVann/react-native-fast-image/blob/master/README.md

通過上面的幾個 Image 屬性分析,綜合來看,Image 組件對圖片的管理能力仍是比較弱的,社區上有個 Image 組件的替代品:react-native-fast-image

它的底層用的是 🔗 iOS 的 SDWebImage 🔗 Android 的 Glide 。這兩個明星圖片下載管理庫,原生開發同窗確定很熟悉,在緩存管理,加載優先級和內存優化上都有不錯的表現。並且這些屬性都是雙平臺可用,這個庫都封裝好了,可是官網上只有基礎功能的安裝和配置,若是想引入一些功能(好比說支持 WebP),仍是須要查看 SDWebImage 和 Glide 的文檔的。

引入前我仍是想提醒一下,React Native 的 Android Image 組件底層封裝了 FaceBook 的 Fresco,引入這個庫至關於又引入了 Glide,包體積不可避免的會變大,因此引入以前可能還要均衡一下。

5️⃣ 圖片服務器輔助

前面說的都是從 React Native 側優化圖片,可是一個產品歷來不是單打獨鬥,藉助服務端的力量其實能夠省不少事。

1.使用 WebP

WebP 的優點不用我多說,一樣的視覺效果,圖片體積會明顯減小。並且能夠顯著減少 CodePush 熱更新包的體積(熱更新包裏,圖片佔用 90% 以上的體積)。

雖然 WebP 在前端解壓耗時可能會多一點點,可是考慮到傳輸體積縮小會縮短網絡下載時間,總體的收益仍是不錯的。

2.圖牀定製圖片

通常比較大的企業都有內建圖牀和 CDN 服務,會提供一些自定製圖片的功能,好比說指定圖片寬高,控制圖片質量。固然一些比較優秀的第三方對象存儲也提供這些功能,好比說🔗 七牛雲 圖片處理

借用雲端圖片定製功能,前端能夠輕鬆經過控制 URL 參數控制圖片屬性

好比說 Android 經過 resizeMethodresize 更改圖片字節大小,雖然也能夠解決問題,可是這個算法仍是在前端運行的,仍是會佔用用戶內存資源。咱們把連接改爲:

https://www.imagescloud.com/image.jpg/0/w/100/h/100/q/80
// w: 長爲 100 px
// h: 寬最多爲 100 px
// q: 壓縮質量爲 80

這樣子就能夠把計算轉移到服務端,減小前端的 CPU 佔用,優化前端總體的性能。

<br/>

4、對象建立調用分離

對象建立和調用分離,其實更多的是一種編碼習慣。

咱們知道在 JavaScript 裏,啥都是對象,而在 JS 引擎裏,建立一個對象的時間差很少是調用一個已存在對象的 10 多倍。在絕大部分狀況下,這點兒性能消耗和時間消耗根本不值一提。但在這裏仍是要總結一下,由於這個思惟習慣仍是很重要的。

1️⃣ public class fields 語法綁定回調函數

📄 文檔https://zh-hans.reactjs.org/docs/handling-events.html

做爲一個前端應用,除了渲染界面,另外一個重要的事情就是處理用戶交互,監聽各類事件。因此在組件上綁定各類處理事件也是一個優化點。

在 React 上如何處理事件已是個很是經典的話題了,我搜索了一下,從 React 剛出來時就有這種文章了,動不動就是四五種處理方案,再加上新出的 Hooks,又能玩出更多花樣了。

最多見的綁定方式應該是直接經過箭頭函數處理事件:

class Button extends React.Component {
  handleClick() {
    console.log('this is:', this);
  }

  render() {
    return <button onClick={(e) => this.handleClick(e)}>Click me</button>;
  }
}

但這種語法的問題是每次 Button 組件從新渲染時,都會建立一個 handleClick() 函數,當 re-render 的次數比較多時,會對 JS 引擎形成必定的垃圾回收壓力,會引發必定的性能問題。

🔗 官方文檔裏比較推薦開發者使用 🔗 public class fields 語法 來處理回調函數,這樣的話一個函數只會建立一次,組件 re-render 時不會再次建立:

class Button extends React.Component {
  // 此語法確保 handleClick 內的 this 已被綁定。
  handleClick = () => {
    console.log('this is:', this);
  }

  render() {
    return <button onClick={this.handleClick}>Click me</button>;
  }
}

在實際開發中,通過一些數據對比,因綁定事件方式的不一樣引發的性能消耗基本上是能夠忽略不計的,re-render 次數過多才是性能殺手。但我認爲這個意識仍是有的,畢竟從邏輯上來說,re-render 一次就要建立一個新的函數是真的不必。

2️⃣ public class fields 語法綁定渲染函數

這個其實和第一個差很少,只不過把事件回調函數改爲渲染函數,在 React Native 的 Flatlist 中很常見。

不少新人使用 Flatlist 時,會直接向 renderItem 傳入匿名函數,這樣每次調用 render 函數時都會建立新的匿名函數:

render(){
  <FlatList
    data={items}
    renderItem={({ item }) => <Text>{item.title}</Text>}
  />
}

改爲 public class fields 式的函數時,就能夠避免這個現象了:

renderItem = ({ item }) => <Text>{item.title}</Text>;

render(){
  <FlatList
    data={items}
    renderItem={renderItem}
  />
}

一樣的道理,ListHeaderComponentListFooterComponent 也應該用這樣寫法,預先傳入已經渲染好的 Element,避免 re-render 時從新生成渲染函數,形成組件內部圖片從新加載出現的閃爍現象。

3️⃣ StyleSheet.create 替代 StyleSheet.flatten

📄 文檔https://reactnative.cn/docs/stylesheet/

StyleSheet.create 這個函數,會把傳入的 Object 轉爲優化後的 StyleID,在內存佔用和 Bridge 通訊上會有些優化。

const styles = StyleSheet.create({
  item: {
    color: 'white',
  },
});

console.log(styles.item) // 打印出的是一個整數 ID

在業務開發時,咱們常常會抽出一些公用 UI 組件,而後傳入不一樣的參數,讓 UI 組件展現不同的樣式。

爲了 UI 樣式的靈活性,咱們通常會使用 StyleSheet.flatten,把經過 props 傳入自定義樣式和默認樣式合併爲一個樣式對象:

const styles = StyleSheet.create({
  item: {
    color: 'white',
  },
});

StyleSheet.flatten([styles.item, props.style]) // <= 合併默認樣式和自定義樣式

這樣作的好處就是能夠靈活的控制樣式,問題就是使用這個方法時,會🔗 遞歸遍歷已經轉換爲 StyleID 的樣式對象,而後生成一個新的樣式對象。這樣就會破壞 StyleSheet.create 以前的優化,可能會引發必定的性能負擔。

固然本節不是說不能用 StyleSheet.flatten通用性和高性能不能同時兼得,根據不一樣的業務場景採起不一樣的方案纔是正解。

4️⃣ 避免在 render 函數裏建立新數組/對象

咱們寫代碼時,爲了不傳入 [] 的地方因數據沒拿到傳入 undefined,常常會默認傳入一個空數組:

render() {
  return <ListComponent listData={this.props.list || []}/>
}

其實更好的作法是下面這樣的:

const EMPTY_ARRAY = [];

render() {
    return <ListComponent listData={this.props.list || EMPTY_ARRAY}/>
}

這個其實算不上啥性能優化,仍是前面再三強調的思路:對象建立和調用分離。畢竟每次渲染的時候從新建立一個空的數組/對象,能帶來多大的性能問題?

[] 改成統一的 EMPTY_ARRAY 常量,其實和平常編碼中避免出現 Magic Number 同樣,算一種編程習慣,但我以爲這種優化能夠歸到這個類別裏,因此專門提一下。

<br/>

5、動畫性能優化

動畫流暢很簡單,在大部分的設備上,只要保證 60fps 的幀率就能夠了。但要達到這個目標,在 React Native 上仍是有些問題的,我畫了一張圖,描述了目前 React Native 的基礎架構(0.61 版本)。

  • UI Thread:在 iOS/Android 上專門繪製 UI 的線程
  • JS Thread:咱們寫的業務代碼基本都在這個線程上,React 重繪,處理 HTTP 請求的結果,磁盤數據 IO 等等
  • other Thread:泛指其餘線程,好比說數據請求線程,磁盤 IO 線程等等

上圖咱們能夠很容易的看出,JS 線程太忙了,要作的事情太多了。並且 UI Thread 和 JS Thread 以前通訊是異步的(Async Bridge),只要其它任務一多,就很難保證每一幀都是及時渲染的。

分析清楚了,React Native 動畫優化的方向天然而然就出來了:

  • 減小 JS Thread 和 UI Thread 之間的異步通訊
  • 儘可能減小 JS Thread 側的計算

1️⃣ 開啓 useNativeDrive: true

📄 文檔https://facebook.github.io/react-native/docs/animations#using-the-native-driver

JS Thread 和 UI Thread 之間是經過 JSON 字符串傳遞消息的。對於一些可預測的動畫,好比說點擊一個點贊按鈕,就跳出一個點贊動畫,這種行爲徹底能夠預測的動畫,咱們可使用 useNativeDrive: true 開啓原生動畫驅動。

經過啓用原生驅動,咱們在啓動動畫前就把其全部配置信息都發送到原生端,利用原生代碼在 UI 線程執行動畫,而不用每一幀都在兩端間來回溝通。如此一來,動畫一開始就徹底脫離了 JS 線程,所以此時即使 JS 線程被卡住,也不會影響到動畫了。

使用也很簡單,只要在動畫開始前在動畫配置中加入 useNativeDrive: true 就能夠了:

Animated.timing(this.state.animatedValue, {
  toValue: 1,
  duration: 500,
  useNativeDriver: true // <-- 加上這一行
}).start();

開啓後全部的動畫都會在 Native 線程運行,動畫就會變的很是絲滑順暢。

通過各類暴力測試,使用原生驅動動畫時,基本沒有掉幀現象,可是用 JS 驅動動畫,一旦操做速度加快,就會有掉幀現象。

值得注意的是,useNativeDriver 這個屬性也有着侷限性,只能使用到只有非佈局相關的動畫屬性上,例如 transformopacity。佈局相關的屬性,好比說 height 和 position 相關的屬性,開啓後會報錯。並且前面也說了,useNativeDriver 只能用在可預測的動畫上,好比說跟隨手勢這種動畫,useNativeDriver 就用不了的。

2️⃣ 使用 setNativeProps

📄 文檔https://facebook.github.io/react-native/docs/direct-manipulation

setNativeProps 這個屬性,至關於直接操做瀏覽器的 DOM。React 官方通常是不推薦直接操做 DOM 的,但業務場景變幻無窮,總會遇到一些場景不得不操做 DOM,在React Native 裏也是一樣的道理。

好比說下面的動圖,在屏幕中上下滾動時,y 軸上的偏移能夠經過 ScrollView#onScroll 屬性開啓 useNativeDrive: true 來優化滾動體驗。可是咱們能夠看到,隨着上下滑動,圓圈裏的數字也是隨之變化的。

若是把數字存在 this.state 裏, 每次滑動不可避免的要進行大量的 setState,React 端會進行大量的重繪操做,可能會引發掉幀。咱們這裏就能夠用 setNativeProps,避免 React 端重繪,至關於直接修改 DOM 上的數字,這樣可讓動畫更加流暢。

3️⃣ 使用 InteractionManager

📄 文檔https://facebook.github.io/react-native/docs/interactionmanager

原生應用感受如此流暢的一個重要緣由就是在互動和動畫的過程當中避免繁重的操做。

在 React Native 裏,JS 線程太忙了,啥都要幹,咱們能夠把一些繁重的任務放在 InteractionManager.runAfterInteractions() 裏,確保在執行前全部的交互和動畫都已經處理完畢。

InteractionManager.runAfterInteractions(() => {
  // ...須要長時間同步執行的任務...
});

在 React Native 官方提供的組件裏,PanResponder、Animated,VirtualizedList 都用了 InteractionManager,爲的就是平衡複雜任務和交互動畫之間的執行時機。

4️⃣ 使用 react-native-reanimated 和 react-native-gesture-handler

📺 視頻教程https://www.youtube.com/channel/UC806fwFWpiLQV5y-qifzHnA

📄 react-native-gesture-handler 文檔https://github.com/software-mansion/react-native-gesture-handler

📄 react-native-reanimated 文檔https://github.com/software-mansion/react-native-reanimated

這兩個庫是被 Youtube 一個自由軟件開發者博主 🔗 William Candillon 安利的,後面查了一下,也是 Expo 默認內置動畫庫和手勢庫。

這兩個庫目的就是替代 React Native 官方提供的🔗 手勢庫🔗 動畫庫,除了 API 更加友好,我認爲最大的優點是:手勢動畫是在 UI Thread 運行的

咱們在前面也說了,useNativeDrive: true 這個屬性,只能用在可預測的動畫上。跟隨手勢的動畫,是沒法使用這個屬性的,因此手勢捕捉和動畫,都是在 JS 側動態計算的。

咱們舉一個簡單的例子:小球跟隨手勢移動

咱們先看看 React Native 官方提供的手勢動畫,能夠看到 JS Thread 有大量的計算,計算結果再異步傳輸到 UI Thread,稍微有些風吹草動,就會引發掉幀。

若是使用 react-native-gesture-handler,手勢捕捉和動畫都是 UI Thread 進行的,脫離 JS Thread 計算和異步線程通訊,流暢度天然大大提高:

因此說,若是要用 React Native 構建複雜的手勢動畫,使用 react-native-gesture-handlerreact-native-reanimated,是一個不錯的選擇,能夠大幅度提升動畫的流暢度。

5️⃣ 使用 BindingX

📄 BindingX 文檔https://alibaba.github.io/bindingx/guide/cn_introduce

BindingX 是阿里開源的一個框架,用來解決 weex React Native 上富交互問題,核心思路是將"交互行爲"以表達式的方式描述,並提早預置到 Native,避免在行爲觸發時 JS 與 Native 的頻繁通訊。

固然,引入上面幾個第三方庫會確定會帶來必定的學習成本。對於複雜交互的頁面,有的團隊可能會採用原生組件來代替,好比說🔗 美團外賣就會用原生組件去實現精細動畫和強交互模塊,因此具體使用還要看團隊的技術儲備和 APP 場景。

<br/>

6、長列表性能優化

在 React Native 開發中,最容易遇到的對性能有必定要求場景就是長列表了。在平常業務實踐中,優化作好後,千條數據渲染仍是沒啥問題的。

虛擬列表前端一直是個經典的話題,核心思想也很簡單:只渲染當前展現和即將展現的 View,距離遠的 View 用空白 View 展現,從而減小長列表的內存佔用。

在 React Native 官網上,🔗 列表配置優化其實說的很好了,咱們基本上只要瞭解清楚幾個配置項,而後靈活配置就好。可是問題就出在「瞭解清楚」這四個字上,本節我會結合圖文,給你們講述清楚這幾個配置。

1️⃣ 各類列表間的關係

React Native 有好幾個列表組件,先簡單介紹一下:

  • ScrollView:會把視圖裏的全部 View 渲染,直接對接 Native 的滾動列表
  • VirtualizedList:虛擬列表核心文件,使用 ScrollView,長列表優化配置項主要是控制它
  • FlatList:使用 VirtualizedList,實現了一行多列的功能,大部分功能都是 VirtualizedList 提供的
  • SectionList:使用 VirtualizedList,底層使用 VirtualizedSectionList,把二維數據轉爲一維數據

還有一些其餘依賴文件,有個🔗 博文的圖總結的挺好的,我這裏借用它的圖一下:

咱們能夠看出 VirtualizedList 纔是主演,下面咱們結合一些示例代碼,分析它的配置項。

2️⃣ 列表配置項

講以前先寫個小 demo。demo 很是簡單,一個基於 FlatList 的奇偶行顏色不一樣的列表。

export default class App extends React.Component {
  renderItem = item => {
    return (
      <Text
        style={{
          backgroundColor: item.index % 2 === 0 ? 'green' : 'blue',
        }}>
        {'第 ' + (item.index + 1) + ' 個'}
      </Text>
    );
  }

  render() {
    let data = [];
    for (let i = 0; i < 1000; i++) {
      data.push({key: i});
    }

    return (
      <View style={{flex: 1}}>
        <FlatList
	  data={data}
          renderItem={this.renderItem}
          initialNumToRender={3} // 首批渲染的元素數量
          windowSize={3} // 渲染區域高度
          removeClippedSubviews={Platform.OS === 'android'} // 是否裁剪子視圖
	  maxToRenderPerBatch={10} // 增量渲染最大數量
          updateCellsBatchingPeriod={50} // 增量渲染時間間隔
          debug // 開啓 debug 模式
        />
      </View>
    );
  }
}

VirtualizedList 有個 debug 的配置項,開啓後會在視圖右側顯示虛擬列表的顯示狀況。

這個屬性文檔中沒有說,是翻🔗 源碼發現的,我發現開啓它後用來演示講解仍是很方便的,能夠很直觀的學習 initialNumToRender、windowSize、Viewport,Blank areas 等概念。

下面是開啓 debug 後的 demo 截屏:

上面的圖仍是很清晰的,右側 debug 指示條的黃色部分表示內存中 Item,各個屬性咱們再用文字描述一下:

1.initialNumToRender

首批應該渲染的元素數量,剛剛蓋住首屏最好。並且從 debug 指示條能夠看出,這批元素會一直存在於內存中。

2.Viewport

視口高度,就是用戶能看到內容,通常就是設備高度。

3.windowSize

渲染區域高度,通常爲 Viewport 的整數倍。這裏我設置爲 3,從 debug 指示條能夠看出,它的高度是 Viewport 的 3 倍,上面擴展 1 個屏幕高度,下面擴展 1 個屏幕高度。在這個區域裏的內容都會保存在內存裏。

將 windowSize 設置爲一個較小值,能有減少內存消耗並提升性能,可是快速滾動列表時,遇到未渲染的內容的概率會增大,會看到佔位的白色 View。你們能夠把 windowSize 設爲 1 測試一下,100% 會看到佔位 View。

4.Blank areas

空白 View,VirtualizedList 會把渲染區域外的 Item 替換爲一個空白 View,用來減小長列表的內存佔用。頂部和底部均可以有。

上圖是渲染圖,咱們能夠利用 react-devtools 再看看 React 的 Virtual DOM(爲了截屏方便,我把 initialNumToRender 和 windowSize 設爲 1),能夠看出和上面的示意圖是一致的。

5.removeClippedSubviews

這個翻譯過來叫「裁剪子視圖」的屬性,文檔描述不是很清晰,大意是設爲 true 能夠提升渲染速度,可是 iOS 上可能會出現 bug。這個屬性 VirtualizedList 沒有作任何優化,是直接透傳給 ScrollView 的。

在 0.59 版本的一次 🔗 commit 裏,FlatList 默認 Android 開啓此功能,若是你的版本低於 0.59,能夠用如下方式開啓:

removeClippedSubviews={Platform.OS === 'android'}

6.maxToRenderPerBatch 和 updateCellsBatchingPeriod

VirtualizedList 的數據不是一會兒所有渲染的,而是分批次渲染的。這兩個屬性就是控制增量渲染的。

這兩個屬性通常是配合着用的,maxToRenderPerBatch 表示每次增量渲染的最大數量,updateCellsBatchingPeriod 表示每次增量渲染的時間間隔

咱們能夠調節這兩個參數來平衡渲染速度和響應速度。可是,調參做爲一門玄學,很可貴出一個統一的「最佳實踐」,因此咱們在業務中也沒有動過這兩個屬性,直接用的系統默認值。

3️⃣ ListLtems 優化

📄 ListLtems 優化 文檔https://reactnative.cn/docs/optimizing-flatlist-configuration/#list-items

文檔中說了好幾點優化,其實在前文我都介紹過了,這裏再簡單提一下:

1.使用 getItemLayout

若是 FlatList(VirtualizedList)的 ListLtem 高度是固定的,那麼使用 getItemLayout 就很是的合算。

在源碼中(#L1287#L2046),若是不使用 getItemLayout,那麼全部的 Cell 的高度,都要調用 View 的 onLayout 動態計算高度,這個運算是須要消耗時間的;若是咱們使用了 getItemLayout,VirtualizedList 就直接知道了 Cell 的高度和偏移量,省去了計算,節省了這部分的開銷。

在這裏我還想提一下幾個注意點,但願你們使用 getItemLayout 要多注意一下:

  • 若是 ListItem 高度不固定,使用 getItemLayout 返回固定高度時,由於最終渲染高度和預測高度不一致,會出現頁面跳動的問題【🔗 問題連接
  • 若是使用了 ItemSeparatorComponent,分隔線的尺寸也要考慮到 offset 的計算中【🔗 文檔連接
  • 若是 FlatList 使用的時候使用了 ListHeaderComponent,也要把 Header 的尺寸考慮到 offset 的計算中【🔗 官方示例代碼連接

2.Use simple components & Use light components

使用簡單組件,核心就是減小邏輯判斷和嵌套,優化方式能夠參考「2、減輕渲染壓力」的內容。

3.Use shouldComponentUpdate

參考「1、re-render」的內容。

4.Use cached optimized images

參考「3、圖片優化那些事」的內容。

5.Use keyExtractor or key

常規優化點了,能夠看 React 的文檔 🔗 列表 & Key

6.Avoid anonymous function on renderItem

renderItem 避免使用匿名函數,參考「4、對象建立調用分離」的內容。

<br/>

7、React Native 性能優化用到的工具

性能優化工具,本質上仍是調試工具的一個子集。React Native 由於它的特殊性,作一些性能分析和調試時,須要用到 RN/iOS/Android 三端的工具,下面我就列舉一下我日常用到的工具,具體的使用方法不是本文的重點,若有須要可根據關鍵詞自行搜索。

1.React Native 官方調試工具

這個官網說的很清楚了,具體內容可見🔗 直達連接

2.react-devtools

React Native 是跑在原生 APP 上的,佈局查看不能用瀏覽器插件,因此要用這個基於 Electron 的 react-devtools。寫本文時 React Native 最新版本仍是 0.61,不支持最新 V4 版本的 react-devtools,還得安裝舊版本。具體安裝方法可見這個🔗 連接

3.XCode

iOS 開發 IDE,查看分析性能問題時能夠用 instrumentsProfiler 進行調試。

4.Android Studio

Android 開發 IDE,查看性能的話可使用 Android Profiler🔗 官方網站寫的很是詳細。

5.iOS Simulator

iOS 模擬器,它的 Debug 能夠看一些分析內容。

6.Android 真機 -> 開發者選項

Android 開發者選項有很多東西可看,好比說 GPU 渲染分析和動畫調試。真機調試時能夠開啓配合使用。

<br/>

8、推薦閱讀

【React Native 性能優化指南】到此就算寫完了,文中內容可能有不嚴謹 or 錯誤的地方,請各位前端/iOS/Android 大佬多多指教。

全文參考近 50 個連接,全放文末太佔篇幅了,因此我都分散在文章各處了,我以 emoji 表情🔗標記的方式進行提示,你們有疑惑的地方能夠去原文查看。

在此我還要推薦一下我之前寫的關於 Webpack 的文章,兩篇都是全網首創

最後推薦一下個人我的公衆號,「鹵代烴實驗室」,會講一些技術和技術以外的內容,你們感興趣的話能夠關注一波:

原文出處:https://www.cnblogs.com/skychx/p/react-native-performance-optimization-guide.html

相關文章
相關標籤/搜索