轉載自 React Native 性能優化指南【全網最全,值得收藏】,感謝 作者 鹵蛋實驗室 的分享
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()
返回的是 true
,shouldComponentUpdate
卻返回的是 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 組件和代碼寫的結構還是一一對應的。
我們再看看 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 文檔: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 過度繪制調試工具查看最后的渲染結果:
對于 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 上有不同的表現,有的實現了有的沒有實現,用起來非常不順手。
在講解圖片優化前,我們先想一下,一個基本的圖片下載管理庫要實現什么:
- 圖片類型:首先你的主要職責是加載圖片,你起碼能加載多種圖片類型
- 下載管理:在加載多張圖片的場景,能管理好多個請求,可以控制圖片加載的優先級
- 緩存管理:做好三級緩存,不能每個圖片都要請求網絡,均衡好內存緩存和磁盤緩存的策略
- 多圖加載:大量圖片同時渲染時,如何讓圖片迅速加載,減少卡頓
針對上面的 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,都是原圖壓縮后的文件,利于磁盤存儲和網絡傳播,但是在屏幕上展示出來時,就要恢復為原始尺寸了。
比如說一張 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 位色彩深度的圖片
?? 色彩深度 wiki:github.com/DylanVann/r…
色彩深度這個概念其實前面也提了一下,比如說我們常用的帶透明度 PNG 圖片,就是 32 位的:
- R:紅色,占據 8 bit
- G:綠色,占據 8 bit
- B:藍色,占據 8 bit
- A:透明通道,占據 8 bit
為啥推薦使用 32 bit 圖片呢?直接原因有 2 個:
- Android 推薦使用 ?? ARGB_8888 格式的圖片,因為這種圖片顯示效果更好
- iOS GPU 只支持加載 32 bit 的圖片。如果是其他格式的(比如說 24 bit 的 jpg),會先在 CPU 里轉為 32 bit,再傳給 GPU
雖然推薦 32 bit 圖片,但是說實話,這個對前端開發是不可控的,因為圖片來源一般就 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 通過 resizeMethod
的 resize
更改圖片字節大小,雖然也可以解決問題,但是這個算法還是在前端運行的,還是會占用用戶內存資源。我們把鏈接改成:
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}
/>
}
復制代碼
同樣的道理,ListHeaderComponent
和 ListFooterComponent
也應該用這樣寫法,預先傳入已經渲染好的 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 版本)。
- 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
開啟原生動畫驅動。
通過啟用原生驅動,我們在啟動動畫前就把其所有配置信息都發送到原生端,利用原生代碼在 UI 線程執行動畫,而不用每一幀都在兩端間來回溝通。如此一來,動畫一開始就完全脫離了 JS 線程,因此此時即便 JS 線程被卡住,也不會影響到動畫了。
使用也很簡單,只要在動畫開始前在動畫配置中加入 useNativeDrive: true
就可以了:
Animated.timing(this.state.animatedValue, {
toValue: 1,
duration: 500,
useNativeDriver: true // <-- 加上這一行
}).start();
復制代碼
開啟后所有的動畫都會在 Native 線程運行,動畫就會變的非常絲滑順暢。
經過各種暴力測試,使用原生驅動動畫時,基本沒有掉幀現象,但是用 JS 驅動動畫,一旦操作速度加快,就會有掉幀現象。
值得注意的是,useNativeDriver
這個屬性也有著局限性,只能使用到只有非布局相關的動畫屬性上,例如 transform
和 opacity
。布局相關的屬性,比如說 height 和 position 相關的屬性,開啟后會報錯。而且前面也說了,useNativeDriver
只能用在可預測的動畫上,比如說跟隨手勢這種動畫,useNativeDriver
就用不了的。
2?? 使用 setNativeProps
?? 文檔:facebook.github.io/react-nativ…
setNativeProps
這個屬性,相當于直接操作瀏覽器的 DOM。React 官方一般是不推薦直接操作 DOM 的,但業務場景千變萬化,總會遇到一些場景不得不操作 DOM,在React Native 里也是同樣的道理。
比如說下面的動圖,在屏幕中上下滾動時,y 軸上的偏移可以通過 ScrollView#onScroll
屬性開啟 useNativeDrive: true
來優化滾動體驗。但是我們可以看到,隨著上下滑動,圓圈里的數字也是隨之變化的。
如果把數字存在 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-gesture-handler,手勢捕捉和動畫都是 UI Thread 進行的,脫離 JS Thread 計算和異步線程通信,流暢度自然大大提升:
所以說,如果要用 React Native 構建復雜的手勢動畫,使用 react-native-gesture-handler 和 react-native-reanimated,是一個不錯的選擇,可以大幅度提高動畫的流暢度。
5?? 使用 BindingX
?? BindingX 文檔:alibaba.github.io/bindingx/gu…
BindingX 是阿里開源的一個框架,用來解決 weex
和 React Native
上富交互問題,核心思路是將"交互行為"以表達式的方式描述,并提前預置到 Native,避免在行為觸發時 JS 與 Native 的頻繁通信。
當然,引入上面幾個第三方庫會肯定會帶來一定的學習成本。對于復雜交互的頁面,有的團隊可能會采用原生組件來代替,比如說?? 美團外賣就會用原生組件去實現精細動畫和強交互模塊,所以具體使用還要看團隊的技術儲備和 APP 場景。
六、長列表性能優化
在 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 表示每次增量渲染的時間間隔。
我們可以調節這兩個參數來平衡渲染速度和響應速度。但是,調參作為一門玄學,很難得出一個統一的「最佳實踐」,所以我們在業務中也沒有動過這兩個屬性,直接用的系統默認值。
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,查看分析性能問題時可以用 instruments 和 Profiler 進行調試。
4.Android Studio
Android 開發 IDE,查看性能的話可以使用 Android Profiler,?? 官方網站寫的非常詳細。
5.iOS Simulator
iOS 模擬器,它的 Debug 可以看一些分析內容。
6.Android 真機 -> 開發者選項
Android 開發者選項有不少東西可看,比如說 GPU 渲染分析和動畫調試。真機調試時可以開啟配合使用。