React Native 效能優化指南【全網最全,值得收藏】

鹵代烴發表於2020-01-09

2020 年談 React Native,在日新月異的前端圈,可能算比較另類了。文章動筆之前我也猶豫過,但是想到寫技術文章又不是趕時髦,啥新潮寫啥,所以還是動筆寫了這篇 React Native 效能優化的文章。

本文談到的 React Native 效能優化,還沒到修改 React Native 原始碼那種地步,所以通用性很強,對大部分 RN 開發者來說都用得著。

本文的內容,一部分是 React/RN/Android/iOS 官方推薦的優化建議,一部分是啃原始碼發現的優化點,還有一部分是可以解決一些效能瓶頸的優秀的開源框架。本文總結的內容你很少在網路上看到,所以看完後一定會有所收穫。如果覺得寫的不錯,請不要吝嗇你的贊,把這篇 1w 多字的文章分享出去,讓更多的人看到。

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


目錄:

  • 一、減少 re-render
  • 二、減輕渲染壓力
  • 三、圖片優化那些事
  • 四、物件建立呼叫分離
  • 五、動畫效能優化
  • 六、長列表效能優化
  • 七、React Native 效能優化用到的工具
  • 八、推薦閱讀

一、減少 re-render

因為 React Native 也是 React 生態系統的一份子,所以很多 React 的優化技巧可以用到這裡,所以文章剛開始先從大家最熟悉的地方開始。

對於 React 來說,減少 re-render 可以說是收益最高的事情了。

1️⃣ shouldComponentUpdate

? 文件react.docschina.org/docs/optimi…

簡單式例:

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,如果有遺漏,就有可能出現資料和檢視不統一的情況。所以使用的時候一定非常小心。

2️⃣️ React.memo

? 文件react.docschina.org/docs/react-…

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

? 文件react.docschina.org/docs/react-…

簡單式例:

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 優化
  • ......

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


二、減輕渲染壓力

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 Native 效能優化指南【全網最全,值得收藏】

從上圖中可以看出,React 元件和程式碼寫的結構還是一一對應的。

我們再看看 React Native 渲染到原生檢視後的巢狀層級(iOS 用 Debug View Hierarchay,Android 用 Layout Inspector):

React Native 效能優化指南【全網最全,值得收藏】

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

如果我們翻一翻 React Native 的原始碼,就會發現 React Native Android UI 佈局前,會對只有佈局屬性的 View(LAYOUT_ONLY_PROPS 原始碼)進行過濾,這樣可以減少 View 節點和巢狀,對碎片化的 Android 更加友好。

通過這個小小的例子我們可以看出,React 元件對映到原生 View 時,並不是一一對應的,我們瞭解了這些知識後,可以如何優化佈局呢?

1️⃣ 使用 React.Fragment 避免多層巢狀

? React Fragments 文件zh-hans.reactjs.org/docs/fragme…

我們先從最熟悉的地方講起——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 過度繪製除錯工具檢視最後的渲染結果:

React Native 效能優化指南【全網最全,值得收藏】

對於 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 過度繪製的細節太多了,一般頁面不需要這種精細化管理,長列表優化時可以考慮一下這個方向。


三、圖片優化那些事

效能優化的另一個大頭就是圖片。這裡的圖片優化不僅僅指減少圖片大小,減少 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 效能優化——圖片記憶體優化

比如說一張 1024x768 的 png 圖片,可能磁碟空間就十幾 kb,不考慮解析度等問題,載入到記憶體裡,就要佔用 3 Mb。

// 不同的解析度/資料夾/編碼格式,都會帶來數值差異
// 下面的計算只是最一般的場景,領會精神即可

(1024 * 768 * 4 * 8) / (8 * 1024 * 1024) = 3 MB
(長 * 寬 * 每個畫素佔用位元組數) / (8 * 1024 * 1024) = 3 MB
複製程式碼

上面只是 1024x768 的圖片,如果圖片尺寸增加一倍,圖片在記憶體裡的大小是按平方倍數增長的,數量一多後,記憶體佔用還是很恐怖的。

在多圖載入的場景裡,經過實踐,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 位色彩深度的圖片

? 色彩深度 wikigithub.com/DylanVann/r…

色彩深度這個概念其實前面也提了一下,比如說我們常用的帶透明度 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

雖然推薦 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 文件github.com/DylanVann/r…

經過上面的幾個 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 佔用,優化前端整體的效能。


四、物件建立呼叫分離

物件建立和呼叫分離,其實更多的是一種編碼習慣。

我們知道在 JavaScript 裡,啥都是物件,而在 JS 引擎裡,建立一個物件的時間差不多是呼叫一個已存在物件的 10 多倍。在絕大部分情況下,這點兒效能消耗和時間消耗根本不值一提。但在這裡還是要總結一下,因為這個思維習慣還是很重要的。

1️⃣ public class fields 語法繫結回撥函式

? 文件zh-hans.reactjs.org/docs/handli…

作為一個前端應用,除了渲染介面,另一個重要的事情就是處理使用者互動,監聽各種事件。所以在元件上繫結各種處理事件也是一個優化點。

在 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

? 文件reactnative.cn/docs/styles…

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 一樣,算一種程式設計習慣,但我覺得這種優化可以歸到這個類別裡,所以專門提一下。


五、動畫效能優化

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

React Native 效能優化指南【全網最全,值得收藏】

  • 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

? 文件facebook.github.io/react-nativ…

JS Thread 和 UI Thread 之間是通過 JSON 字串傳遞訊息的。對於一些可預測的動畫,比如說點選一個點贊按鈕,就跳出一個點贊動畫,這種行為完全可以預測的動畫,我們可以使用 useNativeDrive: true 開啟原生動畫驅動。

React Native 效能優化指南【全網最全,值得收藏】

通過啟用原生驅動,我們在啟動動畫前就把其所有配置資訊都傳送到原生端,利用原生程式碼在 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

? 文件facebook.github.io/react-nativ…

setNativeProps 這個屬性,相當於直接操作瀏覽器的 DOM。React 官方一般是不推薦直接操作 DOM 的,但業務場景千變萬化,總會遇到一些場景不得不操作 DOM,在React Native 裡也是同樣的道理。

比如說下面的動圖,在螢幕中上下滾動時,y 軸上的偏移可以通過 ScrollView#onScroll 屬性開啟 useNativeDrive: true 來優化滾動體驗。但是我們可以看到,隨著上下滑動,圓圈裡的數字也是隨之變化的。

React Native 效能優化指南【全網最全,值得收藏】

如果把數字存在 this.state 裡, 每次滑動不可避免的要進行大量的 setState,React 端會進行大量的重繪操作,可能會引起掉幀。我們這裡就可以用 setNativeProps,避免 React 端重繪,相當於直接修改 DOM 上的數字,這樣可以讓動畫更加流暢。

3️⃣ 使用 InteractionManager

? 文件facebook.github.io/react-nativ…

原生應用感覺如此流暢的一個重要原因就是在互動和動畫的過程中避免繁重的操作。

在 React Native 裡,JS 執行緒太忙了,啥都要幹,我們可以把一些繁重的任務放在 InteractionManager.runAfterInteractions() 裡,確保在執行前所有的互動和動畫都已經處理完畢。

InteractionManager.runAfterInteractions(() => {
  // ...需要長時間同步執行的任務...
});
複製程式碼

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

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

? 視訊教程www.youtube.com/channel/UC8…

? react-native-gesture-handler 文件github.com/software-ma…

? react-native-reanimated 文件github.com/software-ma…

這兩個庫是被 Youtube 一個自由軟體開發者博主 ? William Candillon 安利的,後面查了一下,也是 Expo 預設內建動畫庫和手勢庫。

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

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

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

我們先看看 React Native 官方提供的手勢動畫,可以看到 JS Thread 有大量的計算,計算結果再非同步傳輸到 UI Thread,稍微有些風吹草動,就會引起掉幀。

React Native 效能優化指南【全網最全,值得收藏】

如果使用 react-native-gesture-handler,手勢捕捉和動畫都是 UI Thread 進行的,脫離 JS Thread 計算和非同步執行緒通訊,流暢度自然大大提升:

React Native 效能優化指南【全網最全,值得收藏】

所以說,如果要用 React Native 構建複雜的手勢動畫,使用 react-native-gesture-handlerreact-native-reanimated,是一個不錯的選擇,可以大幅度提高動畫的流暢度。

5️⃣ 使用 BindingX

? BindingX 文件alibaba.github.io/bindingx/gu…

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


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

React Native 效能優化指南【全網最全,值得收藏】


六、長列表效能優化

在 React Native 開發中,最容易遇到的對效能有一定要求場景就是長列表了。在日常業務實踐中,優化做好後,千條資料渲染還是沒啥問題的。

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

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

1️⃣ 各種列表間的關係

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

  • ScrollView:會把檢視裡的所有 View 渲染,直接對接 Native 的滾動列表
  • VirtualizedList:虛擬列表核心檔案,使用 ScrollView,長列表優化配置項主要是控制它
  • FlatList:使用 VirtualizedList,實現了一行多列的功能,大部分功能都是 VirtualizedList 提供的
  • SectionList:使用 VirtualizedList,底層使用 VirtualizedSectionList,把二維資料轉為一維資料

還有一些其他依賴檔案,有個? 博文的圖總結的挺好的,我這裡借用它的圖一下:

React Native 效能優化指南【全網最全,值得收藏】

我們可以看出 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 截圖:

React Native 效能優化指南【全網最全,值得收藏】

上面的圖還是很清晰的,右側 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),可以看出和上面的示意圖是一致的。

React Native 效能優化指南【全網最全,值得收藏】

5.removeClippedSubviews

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

在 0.59 版本的一次 ? commit 裡,FlatList 預設 Android 開啟此功能,如果你的版本低於 0.59,可以用以下方式開啟:

removeClippedSubviews={Platform.OS === 'android'}
複製程式碼

6.maxToRenderPerBatch 和 updateCellsBatchingPeriod

VirtualizedList 的資料不是一下子全部渲染的,而是分批次渲染的。這兩個屬性就是控制增量渲染的。

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

我們可以調節這兩個引數來平衡渲染速度和響應速度。但是,調參作為一門玄學,很難得出一個統一的「最佳實踐」,所以我們在業務中也沒有動過這兩個屬性,直接用的系統預設值。

2️⃣️ ListLtems 優化

? ListLtems 優化 文件reactnative.cn/docs/optimi…

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

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

使用簡單元件,核心就是減少邏輯判斷和巢狀,優化方式可以參考「二、減輕渲染壓力」的內容。

3.Use shouldComponentUpdate

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

4.Use cached optimized images

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

5.Use keyExtractor or key

常規優化點了,可以看 React 的文件 ? 列表 & Key

6.Avoid anonymous function on renderItem

renderItem 避免使用匿名函式,參考「四、物件建立呼叫分離」的內容。


七、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 進行除錯。

React Native 效能優化指南【全網最全,值得收藏】

4.Android Studio

Android 開發 IDE,檢視效能的話可以使用 Android Profiler? 官方網站寫的非常詳細。

5.iOS Simulator

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

React Native 效能優化指南【全網最全,值得收藏】

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

Android 開發者選項有不少東西可看,比如說 GPU 渲染分析和動畫除錯。真機除錯時可以開啟配合使用。


八、推薦閱讀

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

全文參考近 50 個連結,全放文末太佔篇幅了,所以我都分散在文章各處了,我以 emoji 表情? 標記的方式進行提示,大家有疑惑的地方可以去原文檢視。

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


最後推薦一下我的個人公眾號,「鹵代烴實驗室」,會講一些技術和技術之外的內容,大家感興趣的話可以關注一波:

React Native 效能優化指南【全網最全,值得收藏】

相關文章