RN版本:0.64
系統:Win10
前言
盡管facebook已經盡可能地優化 React Native 性能來了,但是,總還是有一些地方有所欠缺,以及在某些場合 React Native 還不能夠替我們決定如何進行優化,因此人工的干預依然是必要的。
1.減少頁面內重繪制
在 React 應用中,當某個組件的狀態發生變化時,它會以該組件為根,重新渲染整個組件子樹。如要避免不必要的子組件的重渲染,有以下途徑:
- 實現shouldComponentUpdate函數來指明在什么樣的確切條件下,希望組件得到重繪
// 示例
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.memo 和 useMemo
// React.memo
// 示例
// 默認比較函數
const MemoButton = React.memo(function Button(props) {
return <button color={this.props.color} />;
});
// 自定義比較函數
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);
//解決因函數更新而渲染自己的問題,就可以使用useMemo,使用它將函數重新封裝
const onClick=useMemo(()=>{
return ()=>{
console.log(m)
}
},[m])
//等價于
const onClick=useCallback(()=>{
console.log(m)
},[m])
- 如果編寫的是純粹的組件(界面完全由 props 和 state 所決定),可以使用PureComponent來獲得更好的性能
// 示例
class PureComponentButton extends React.PureComponent {
render() {
return <button color={this.props.color} />;
}
}
2.減輕渲染壓力
- 使用 React.Fragment 避免多層嵌套
// 示例
render() {
return (
<React.Fragment>
<ChildA />
<ChildB />
<ChildC />
</React.Fragment>
);
}
// 或者使用 Fragment 短語法
render() {
return (
<>
<ChildA />
<ChildB />
<ChildC />
</>
);
}
- 減少 GPU 過度繪制
- 減少背景色的重復設置:每個 View 都設置背景色的話,在 Android上會造成非常嚴重的過度繪制;并且只有布局屬性時,React Native 還會減少 Android 的布局嵌套
- 避免設置半透明顏色:半透明色區域 iOS Android 都會引起過度繪制
- 避免設置圓角:圓角部位 iOS Android 都會引起過度繪制
- 避免設置陰影:陰影區域 iOS Android 都會引起過度繪制
3.對象創建調用分離
在 JS 引擎里,創建一個對象的時間差不多是調用一個已存在對象的 10 多倍。在絕大部分情況下,這點兒性能消耗和時間消耗不值一提。但在這里還是要總結一下,因為這個思維習慣還是很重要的。
1. public class fields 語法綁定回調函數項
在 React 上如何處理事件已經是個非常經典的話題了,最常見的綁定方式應該是直接通過箭頭函數處理事件:
class Button extends React.Component {
handleClick() {
console.log('this is:', this);
}
render() {
return <button onClick={(e) => this.handleClick(e)}>Click me</button>;
}
}
但這種語法的問題是每次 Button 組件重新渲染時,都會創建一個 handleClick() 函數,當重繪制的次數比較多時,會對 JS 引擎造成一定的垃圾回收壓力,會引起一定的性能問題。
官方文檔里比較推薦開發者使用 public class fields 語法 來處理回調函數,這樣的話一個函數只會創建一次,組件 重繪制時不會再次創建:
class Button extends React.Component {
// 此語法確保 handleClick 內的 this 已被綁定。
handleClick = () => {
console.log('this is:', this);
}
render() {
return <button onClick={this.handleClick}>Click me</button>;
}
}
在實際開發中,經過一些數據對比,因綁定事件方式的不同引起的性能消耗基本上是可以忽略不計的,重繪制 次數過多才是性能殺手。
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
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}/>
}
4.動畫性能優化
1. 開啟 useNativeDrive: true
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 線程運行,動畫就會變的非常順暢。
值得注意的是,useNativeDriver 這個屬性也有著局限性,只能使用到只有非布局相關的動畫屬性上,例如 transform 和 opacity。布局相關的屬性,比如說 height 和 position 相關的屬性,開啟后會報錯。而且前面也說了,useNativeDriver 只能用在可預測的動畫上,比如說跟隨手勢這種動畫,useNativeDriver 就用不了的。
5.長列表性能優化
在 React Native 開發中,最容易遇到的對性能有一定要求場景就是長列表了。在日常業務實踐中,優化做好后,千條數據渲染還是沒啥問題的。
虛擬列表前端一直是個經典的話題,核心思想也很簡單:只渲染當前展示和即將展示的 View,距離遠的 View 用空白 View 展示,從而減少長列表的內存占用。(這一點和Android的RecycleView比較類型)
在 React Native 官網上, 列表配置優化其實說的很好了,我們基本上只要了解清楚幾個配置項,然后靈活配置就好。但是問題就出在「了解清楚」這四個字上,本節我會結合圖文,給大家講述清楚這幾個配置。
1.各種列表間的關系
React Native 有好幾個列表組件,先簡單介紹一下:
ScrollView:會把視圖里的所有 View 渲染,直接對接 Native 的滾動列表
VirtualizedList:虛擬列表核心文件,使用 ScrollView,長列表優化配置項主要是控制它
FlatList:使用 VirtualizedList,實現了一行多列的功能,大部分功能都是 VirtualizedList 提供的
SectionList:使用 VirtualizedList,底層使用 VirtualizedSectionList,把二維數據轉為一維數據
2.列表配置項
一個基于 FlatList 的奇偶行顏色不同的列表
// 一個基于 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>
);
}
}
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 優化
1.使用 getItemLayout
如果 FlatList(VirtualizedList)的 ListLtem 高度是固定的,那么使用 getItemLayout 就非常的合算。
如果不使用 getItemLayout,那么所有的 Cell 的高度,都要調用 View 的 onLayout 動態計算高度,這個運算是需要消耗時間的;如果我們使用了 getItemLayout,VirtualizedList 就直接知道了 Cell 的高度和偏移量,省去了計算,節省了這部分的開銷。
如果 ListItem 高度不固定,使用 getItemLayout 返回固定高度時,因為最終渲染高度和預測高度不一致,會出現頁面跳動的問題
如果使用了 ItemSeparatorComponent,分隔線的尺寸也要考慮到 offset 的計算中
如果 FlatList 使用的時候使用了 ListHeaderComponent,也要把 Header 的尺寸考慮到 offset 的計算中
2.Use simple components & Use light components
使用簡單組件,核心就是減少邏輯判斷和嵌套
3.Use shouldComponentUpdate
4.Use cached optimized images
5.Use keyExtractor or key
常規優化點了,可以看 React 的文檔 列表 & Key。
6.Avoid anonymous function on renderItem
renderItem 避免使用匿名函數