React Native從零單排5 性能調優

RN版本:0.64
系統:Win10

前言

盡管facebook已經盡可能地優化 React Native 性能來了,但是,總還是有一些地方有所欠缺,以及在某些場合 React Native 還不能夠替我們決定如何進行優化,因此人工的干預依然是必要的。

1.減少頁面內重繪制

在 React 應用中,當某個組件的狀態發生變化時,它會以該組件為根,重新渲染整個組件子樹。如要避免不必要的子組件的重渲染,有以下途徑:

  1. 實現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} />;
 }
}
  1. 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])
  1. 如果編寫的是純粹的組件(界面完全由 props 和 state 所決定),可以使用PureComponent來獲得更好的性能
// 示例
class PureComponentButton extends React.PureComponent {
  render() {
    return <button color={this.props.color} />;
  }
}

2.減輕渲染壓力

  1. 使用 React.Fragment 避免多層嵌套
// 示例
render() {
  return (
    <React.Fragment>
      <ChildA />
      <ChildB />
      <ChildC />
    </React.Fragment>
  );
}

// 或者使用 Fragment 短語法
render() {
  return (
    <>
      <ChildA />
      <ChildB />
      <ChildC />
    </>
  );
}
  1. 減少 GPU 過度繪制
    1. 減少背景色的重復設置:每個 View 都設置背景色的話,在 Android上會造成非常嚴重的過度繪制;并且只有布局屬性時,React Native 還會減少 Android 的布局嵌套
    2. 避免設置半透明顏色:半透明色區域 iOS Android 都會引起過度繪制
    3. 避免設置圓角:圓角部位 iOS Android 都會引起過度繪制
    4. 避免設置陰影:陰影區域 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 避免使用匿名函數

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容