React Native從零單排5 性能調(diào)優(yōu)

RN版本:0.64
系統(tǒng):Win10

前言

盡管facebook已經(jīng)盡可能地優(yōu)化 React Native 性能來(lái)了蒿秦,但是,總還是有一些地方有所欠缺规求,以及在某些場(chǎng)合 React Native 還不能夠替我們決定如何進(jìn)行優(yōu)化凌外,因此人工的干預(yù)依然是必要的啊片。

1.減少頁(yè)面內(nèi)重繪制

在 React 應(yīng)用中,當(dāng)某個(gè)組件的狀態(tài)發(fā)生變化時(shí)刘绣,它會(huì)以該組件為根樱溉,重新渲染整個(gè)組件子樹。如要避免不必要的子組件的重渲染,有以下途徑:

  1. 實(shí)現(xiàn)shouldComponentUpdate函數(shù)來(lái)指明在什么樣的確切條件下纬凤,希望組件得到重繪
// 示例
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
// 示例
// 默認(rèn)比較函數(shù)
const MemoButton = React.memo(function Button(props) {
  return <button color={this.props.color} />;
});
// 自定義比較函數(shù)
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);

//解決因函數(shù)更新而渲染自己的問題福贞,就可以使用useMemo,使用它將函數(shù)重新封裝
const onClick=useMemo(()=>{
    return ()=>{
      console.log(m)
    }
  },[m])
//等價(jià)于
const onClick=useCallback(()=>{
      console.log(m)
  },[m])
  1. 如果編寫的是純粹的組件(界面完全由 props 和 state 所決定),可以使用PureComponent來(lái)獲得更好的性能
// 示例
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 短語(yǔ)法
render() {
  return (
    <>
      <ChildA />
      <ChildB />
      <ChildC />
    </>
  );
}
  1. 減少 GPU 過(guò)度繪制
    1. 減少背景色的重復(fù)設(shè)置:每個(gè) View 都設(shè)置背景色的話停士,在 Android上會(huì)造成非常嚴(yán)重的過(guò)度繪制挖帘;并且只有布局屬性時(shí),React Native 還會(huì)減少 Android 的布局嵌套
    2. 避免設(shè)置半透明顏色:半透明色區(qū)域 iOS Android 都會(huì)引起過(guò)度繪制
    3. 避免設(shè)置圓角:圓角部位 iOS Android 都會(huì)引起過(guò)度繪制
    4. 避免設(shè)置陰影:陰影區(qū)域 iOS Android 都會(huì)引起過(guò)度繪制

3.對(duì)象創(chuàng)建調(diào)用分離

在 JS 引擎里向瓷,創(chuàng)建一個(gè)對(duì)象的時(shí)間差不多是調(diào)用一個(gè)已存在對(duì)象的 10 多倍肠套。在絕大部分情況下,這點(diǎn)兒性能消耗和時(shí)間消耗不值一提猖任。但在這里還是要總結(jié)一下你稚,因?yàn)檫@個(gè)思維習(xí)慣還是很重要的。

1. public class fields 語(yǔ)法綁定回調(diào)函數(shù)項(xiàng)

在 React 上如何處理事件已經(jīng)是個(gè)非常經(jīng)典的話題了朱躺,最常見的綁定方式應(yīng)該是直接通過(guò)箭頭函數(shù)處理事件:

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

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

但這種語(yǔ)法的問題是每次 Button 組件重新渲染時(shí)刁赖,都會(huì)創(chuàng)建一個(gè) handleClick() 函數(shù),當(dāng)重繪制的次數(shù)比較多時(shí)长搀,會(huì)對(duì) JS 引擎造成一定的垃圾回收壓力宇弛,會(huì)引起一定的性能問題。
官方文檔里比較推薦開發(fā)者使用 public class fields 語(yǔ)法 來(lái)處理回調(diào)函數(shù)源请,這樣的話一個(gè)函數(shù)只會(huì)創(chuàng)建一次枪芒,組件 重繪制時(shí)不會(huì)再次創(chuàng)建:

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

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

在實(shí)際開發(fā)中谁尸,經(jīng)過(guò)一些數(shù)據(jù)對(duì)比舅踪,因綁定事件方式的不同引起的性能消耗基本上是可以忽略不計(jì)的,重繪制 次數(shù)過(guò)多才是性能殺手良蛮。

2. public class fields 語(yǔ)法綁定渲染函數(shù)

這個(gè)其實(shí)和第一個(gè)差不多抽碌,只不過(guò)把事件回調(diào)函數(shù)改成渲染函數(shù),在 React Native 的 Flatlist 中很常見决瞳。
很多人使用 Flatlist 時(shí)货徙,會(huì)直接向 renderItem 傳入匿名函數(shù)左权,這樣每次調(diào)用 render 函數(shù)時(shí)都會(huì)創(chuàng)建新的匿名函數(shù):

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

改成 public class fields 式的函數(shù)時(shí),就可以避免這個(gè)現(xiàn)象了:

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

同樣的道理痴颊,ListHeaderComponent 和 ListFooterComponent 也應(yīng)該用這樣寫法赏迟,預(yù)先傳入已經(jīng)渲染好的 Element,避免 re-render 時(shí)重新生成渲染函數(shù)祷舀,造成組件內(nèi)部圖片重新加載出現(xiàn)的閃爍現(xiàn)象瀑梗。

3. StyleSheet.create 替代 StyleSheet.flatten

StyleSheet.create 這個(gè)函數(shù),會(huì)把傳入的 Object 轉(zhuǎn)為優(yōu)化后的 StyleID裳扯,在內(nèi)存占用和 Bridge 通信上會(huì)有些優(yōu)化抛丽。

const styles = StyleSheet.create({
  item: {
    color: 'white',
  },
});
console.log(styles.item) // 打印出的是一個(gè)整數(shù) ID

在業(yè)務(wù)開發(fā)時(shí),我們經(jīng)常會(huì)抽出一些公用 UI 組件饰豺,然后傳入不同的參數(shù)亿鲜,讓 UI 組件展示不一樣的樣式。
為了 UI 樣式的靈活性冤吨,我們一般會(huì)使用 StyleSheet.flatten蒿柳,把通過(guò) props 傳入自定義樣式和默認(rèn)樣式合并為一個(gè)樣式對(duì)象:

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

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

這樣做的好處就是可以靈活的控制樣式,問題就是使用這個(gè)方法時(shí)漩蟆,會(huì) 遞歸遍歷已經(jīng)轉(zhuǎn)換為 StyleID 的樣式對(duì)象垒探,然后生成一個(gè)新的樣式對(duì)象。這樣就會(huì)破壞 StyleSheet.create 之前的優(yōu)化怠李,可能會(huì)引起一定的性能負(fù)擔(dān)圾叼。
當(dāng)然本節(jié)不是說(shuō)不能用 StyleSheet.flatten,通用性和高性能不能同時(shí)兼得捺癞,根據(jù)不同的業(yè)務(wù)場(chǎng)景采取不同的方案才是正解夷蚊。

4. 避免在 render 函數(shù)里創(chuàng)建新數(shù)組/對(duì)象

我們寫代碼時(shí),為了避免傳入 [] 的地方因數(shù)據(jù)沒拿到傳入 undefined髓介,經(jīng)常會(huì)默認(rèn)傳入一個(gè)空數(shù)組:

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

其實(shí)更好的做法是下面這樣的:

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

4.動(dòng)畫性能優(yōu)化

1. 開啟 useNativeDrive: true

JS Thread 和 UI Thread 之間是通過(guò) JSON 字符串傳遞消息的惕鼓。對(duì)于一些可預(yù)測(cè)的動(dòng)畫,比如說(shuō)點(diǎn)擊一個(gè)點(diǎn)贊按鈕唐础,就跳出一個(gè)點(diǎn)贊動(dòng)畫箱歧,這種行為完全可以預(yù)測(cè)的動(dòng)畫,我們可以使用 useNativeDrive: true 開啟原生動(dòng)畫驅(qū)動(dòng)一膨。

通過(guò)啟用原生驅(qū)動(dòng)呀邢,我們?cè)趩?dòng)動(dòng)畫前就把其所有配置信息都發(fā)送到原生端,利用原生代碼在 UI 線程執(zhí)行動(dòng)畫汞幢,而不用每一幀都在兩端間來(lái)回溝通。如此一來(lái)微谓,動(dòng)畫一開始就完全脫離了 JS 線程森篷,因此此時(shí)即便 JS 線程被卡住输钩,也不會(huì)影響到動(dòng)畫了。

使用也很簡(jiǎn)單仲智,只要在動(dòng)畫開始前在動(dòng)畫配置中加入 useNativeDrive: true 就可以了:

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

開啟后所有的動(dòng)畫都會(huì)在 Native 線程運(yùn)行买乃,動(dòng)畫就會(huì)變的非常順暢。

值得注意的是钓辆,useNativeDriver 這個(gè)屬性也有著局限性剪验,只能使用到只有非布局相關(guān)的動(dòng)畫屬性上,例如 transform 和 opacity前联。布局相關(guān)的屬性功戚,比如說(shuō) height 和 position 相關(guān)的屬性,開啟后會(huì)報(bào)錯(cuò)似嗤。而且前面也說(shuō)了啸臀,useNativeDriver 只能用在可預(yù)測(cè)的動(dòng)畫上,比如說(shuō)跟隨手勢(shì)這種動(dòng)畫烁落,useNativeDriver 就用不了的乘粒。

5.長(zhǎng)列表性能優(yōu)化

在 React Native 開發(fā)中,最容易遇到的對(duì)性能有一定要求場(chǎng)景就是長(zhǎng)列表了伤塌。在日常業(yè)務(wù)實(shí)踐中灯萍,優(yōu)化做好后,千條數(shù)據(jù)渲染還是沒啥問題的每聪。

虛擬列表前端一直是個(gè)經(jīng)典的話題旦棉,核心思想也很簡(jiǎn)單:只渲染當(dāng)前展示和即將展示的 View,距離遠(yuǎn)的 View 用空白 View 展示熊痴,從而減少長(zhǎng)列表的內(nèi)存占用他爸。(這一點(diǎn)和Android的RecycleView比較類型)

在 React Native 官網(wǎng)上, 列表配置優(yōu)化其實(shí)說(shuō)的很好了果善,我們基本上只要了解清楚幾個(gè)配置項(xiàng)诊笤,然后靈活配置就好。但是問題就出在「了解清楚」這四個(gè)字上巾陕,本節(jié)我會(huì)結(jié)合圖文讨跟,給大家講述清楚這幾個(gè)配置。

1.各種列表間的關(guān)系
React Native 有好幾個(gè)列表組件鄙煤,先簡(jiǎn)單介紹一下:
ScrollView:會(huì)把視圖里的所有 View 渲染晾匠,直接對(duì)接 Native 的滾動(dòng)列表
VirtualizedList:虛擬列表核心文件,使用 ScrollView梯刚,長(zhǎng)列表優(yōu)化配置項(xiàng)主要是控制它
FlatList:使用 VirtualizedList凉馆,實(shí)現(xiàn)了一行多列的功能,大部分功能都是 VirtualizedList 提供的
SectionList:使用 VirtualizedList,底層使用 VirtualizedSectionList澜共,把二維數(shù)據(jù)轉(zhuǎn)為一維數(shù)據(jù)

2.列表配置項(xiàng)
一個(gè)基于 FlatList 的奇偶行顏色不同的列表

// 一個(gè)基于 FlatList 的奇偶行顏色不同的列表
export default class App extends React.Component {
  renderItem = item => {
    return (
      <Text
        style={{
          backgroundColor: item.index % 2 === 0 ? 'green' : 'blue',
        }}>
        {'第 ' + (item.index + 1) + ' 個(gè)'}
      </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} // 首批渲染的元素?cái)?shù)量
          windowSize={3} // 渲染區(qū)域高度
          removeClippedSubviews={Platform.OS === 'android'} // 是否裁剪子視圖
          maxToRenderPerBatch={10} // 增量渲染最大數(shù)量
          updateCellsBatchingPeriod={50} // 增量渲染時(shí)間間隔
          debug // 開啟 debug 模式, 開啟后會(huì)在視圖右側(cè)顯示虛擬列表的顯示情況向叉。
        />
      </View>
    );
  }
}

1.initialNumToRender
首批應(yīng)該渲染的元素?cái)?shù)量,剛剛蓋住首屏最好嗦董。而且從 debug 指示條可以看出母谎,這批元素會(huì)一直存在于內(nèi)存中。
2.Viewport
視口高度京革,就是用戶能看到內(nèi)容奇唤,一般就是設(shè)備高度。
3.windowSize
渲染區(qū)域高度匹摇,一般為 Viewport 的整數(shù)倍咬扇。這里我設(shè)置為 3,從 debug 指示條可以看出来惧,它的高度是 Viewport 的 3 倍冗栗,上面擴(kuò)展 1 個(gè)屏幕高度,下面擴(kuò)展 1 個(gè)屏幕高度供搀。在這個(gè)區(qū)域里的內(nèi)容都會(huì)保存在內(nèi)存里隅居。
將 windowSize 設(shè)置為一個(gè)較小值,能有減小內(nèi)存消耗并提高性能葛虐,但是快速滾動(dòng)列表時(shí)胎源,遇到未渲染的內(nèi)容的幾率會(huì)增大,會(huì)看到占位的白色 View屿脐。大家可以把 windowSize 設(shè)為 1 測(cè)試一下涕蚤,100% 會(huì)看到占位 View。
4.Blank areas
空白 View的诵,VirtualizedList 會(huì)把渲染區(qū)域外的 Item 替換為一個(gè)空白 View万栅,用來(lái)減少長(zhǎng)列表的內(nèi)存占用。頂部和底部都可以有西疤。
上圖是渲染圖烦粒,我們可以利用 react-devtools 再看看 React 的 Virtual DOM(為了截屏方便,我把 initialNumToRender 和 windowSize 設(shè)為 1)代赁,可以看出和上面的示意圖是一致的扰她。
5.removeClippedSubviews
這個(gè)翻譯過(guò)來(lái)叫「裁剪子視圖」的屬性,文檔描述不是很清晰芭碍,大意是設(shè)為 true 可以提高渲染速度徒役,但是 iOS 上可能會(huì)出現(xiàn) bug。這個(gè)屬性 VirtualizedList 沒有做任何優(yōu)化窖壕,是直接透?jìng)鹘o ScrollView 的忧勿。
在 0.59 版本的一次 commit 里杉女,F(xiàn)latList 默認(rèn) Android 開啟此功能,如果你的版本低于 0.59鸳吸,可以用以下方式開啟:
removeClippedSubviews={Platform.OS === 'android'}
6.maxToRenderPerBatch 和 updateCellsBatchingPeriod
VirtualizedList 的數(shù)據(jù)不是一下子全部渲染的宠纯,而是分批次渲染的。這兩個(gè)屬性就是控制增量渲染的层释。
這兩個(gè)屬性一般是配合著用的,maxToRenderPerBatch 表示每次增量渲染的最大數(shù)量快集,updateCellsBatchingPeriod 表示每次增量渲染的時(shí)間間隔贡羔。

3.ListLtems 優(yōu)化
1.使用 getItemLayout
如果 FlatList(VirtualizedList)的 ListLtem 高度是固定的,那么使用 getItemLayout 就非常的合算个初。
如果不使用 getItemLayout乖寒,那么所有的 Cell 的高度,都要調(diào)用 View 的 onLayout 動(dòng)態(tài)計(jì)算高度院溺,這個(gè)運(yùn)算是需要消耗時(shí)間的楣嘁;如果我們使用了 getItemLayout,VirtualizedList 就直接知道了 Cell 的高度和偏移量珍逸,省去了計(jì)算逐虚,節(jié)省了這部分的開銷。
如果 ListItem 高度不固定谆膳,使用 getItemLayout 返回固定高度時(shí)叭爱,因?yàn)樽罱K渲染高度和預(yù)測(cè)高度不一致,會(huì)出現(xiàn)頁(yè)面跳動(dòng)的問題
如果使用了 ItemSeparatorComponent漱病,分隔線的尺寸也要考慮到 offset 的計(jì)算中
如果 FlatList 使用的時(shí)候使用了 ListHeaderComponent买雾,也要把 Header 的尺寸考慮到 offset 的計(jì)算中
2.Use simple components & Use light components
使用簡(jiǎn)單組件,核心就是減少邏輯判斷和嵌套
3.Use shouldComponentUpdate
4.Use cached optimized images
5.Use keyExtractor or key
常規(guī)優(yōu)化點(diǎn)了杨帽,可以看 React 的文檔 列表 & Key漓穿。
6.Avoid anonymous function on renderItem
renderItem 避免使用匿名函數(shù)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市注盈,隨后出現(xiàn)的幾起案子晃危,更是在濱河造成了極大的恐慌,老刑警劉巖当凡,帶你破解...
    沈念sama閱讀 212,080評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件山害,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡沿量,警方通過(guò)查閱死者的電腦和手機(jī)浪慌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,422評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)朴则,“玉大人权纤,你說(shuō)我怎么就攤上這事钓简。” “怎么了汹想?”我有些...
    開封第一講書人閱讀 157,630評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵外邓,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我古掏,道長(zhǎng)损话,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,554評(píng)論 1 284
  • 正文 為了忘掉前任槽唾,我火速辦了婚禮丧枪,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘庞萍。我一直安慰自己拧烦,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,662評(píng)論 6 386
  • 文/花漫 我一把揭開白布钝计。 她就那樣靜靜地躺著恋博,像睡著了一般。 火紅的嫁衣襯著肌膚如雪私恬。 梳的紋絲不亂的頭發(fā)上债沮,一...
    開封第一講書人閱讀 49,856評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音本鸣,去河邊找鬼秦士。 笑死,一個(gè)胖子當(dāng)著我的面吹牛永高,可吹牛的內(nèi)容都是我干的隧土。 我是一名探鬼主播,決...
    沈念sama閱讀 39,014評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼命爬,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼曹傀!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起饲宛,我...
    開封第一講書人閱讀 37,752評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤皆愉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后艇抠,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體幕庐,經(jīng)...
    沈念sama閱讀 44,212評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,541評(píng)論 2 327
  • 正文 我和宋清朗相戀三年家淤,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了异剥。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,687評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡絮重,死狀恐怖冤寿,靈堂內(nèi)的尸體忽然破棺而出歹苦,到底是詐尸還是另有隱情,我是刑警寧澤督怜,帶...
    沈念sama閱讀 34,347評(píng)論 4 331
  • 正文 年R本政府宣布殴瘦,位于F島的核電站,受9級(jí)特大地震影響号杠,放射性物質(zhì)發(fā)生泄漏蚪腋。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,973評(píng)論 3 315
  • 文/蒙蒙 一姨蟋、第九天 我趴在偏房一處隱蔽的房頂上張望辣吃。 院中可真熱鬧,春花似錦芬探、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,777評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至宵蕉,卻和暖如春酝静,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背羡玛。 一陣腳步聲響...
    開封第一講書人閱讀 32,006評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工别智, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人稼稿。 一個(gè)月前我還...
    沈念sama閱讀 46,406評(píng)論 2 360
  • 正文 我出身青樓薄榛,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親让歼。 傳聞我的和親對(duì)象是個(gè)殘疾皇子敞恋,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,576評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容