reactNative性能優化


頭圖

本文將簡單介紹一下我所收集到的React Native應用優化方法,希望對你有所啟發。很多方法也是適用React web應用的。

包體積優化

無論是熱更新方案走網絡下載js,還是直接將js打進apk,減小js bundle體積都很必要。

走網絡的js體積大影響首次加載速度,打進apk的增加包體積。

  1. 壓縮

為了測試,直接使用react-native init命令生成了一個rn工程,將其中的App.js改為下面這樣簡單的代碼,驗證這樣簡單的代碼打包生成的js bundle體積情況。

import React from 'react';
import {
  Text,
} from 'react-native';

const App: () => React$Node = () => {
  return (
    <Text>1</Text>
  )

};

export default App;

使用 下邊的命令可以打包js bundle

# 非壓縮

react-native bundle --entry-file ./index.js --bundle-output ./testBundle.js --dev true
# 壓縮

react-native bundle --entry-file ./index.js --bundle-output ./testBundle_min.js --dev false


# 查看體積

ls -h testBundle.js testBundle_min.js 

-rw-r--r--  1 bingxiongyi  staff   3.3M Dec 24 11:02 testBundle.js
-rw-r--r--  1 bingxiongyi  staff   629K Dec 24 11:06 testBundle_min.js

可見不壓縮的體積遠遠大於壓縮后的體積,因此壓縮十分必要。將js打包進apk的,默認就會壓縮。

關鍵要注意的是走網絡下載js的這種方式需要壓縮js.

  1. 包拆分

上邊可以看到一個空工程打包出來的js就壓縮后有629k, 他們主要是react-native,react這些框架的代碼。

如果是純的react-native應用,我們可能會直接用react-navigation做路由,這個應用就是生成一個js bundle,打到apk, 框架部分的代碼不會重復生成,自然沒有什么問題。

但是大多情況為了實現熱更新,react-native是嵌入到原生app中的,可能一個頁面就會是一個js bundle, 走網絡下載,如果每個js bundle都包含框架的代碼,必然造成不必要的重復下載。

因此,需要將框架和不常變動的代碼單獨抽取,最好是將這部分代碼打進apk, 這樣業務代碼體積會大大減小。實際發現我們一個包含三個復雜頁面的js bundle業務代碼壓縮體積僅僅180k多。

關於如何拆包可參考React native 拆包

render次數優化

為什么要減少render次數

走到了render並不一定會有真實dom的操作,但是一定會走一次dom diff。react大名鼎鼎的diff算法確實高效,看了一眼vitual-dom原理與簡單實現,確實像大佬們說的,是個O(n)的算法。

需要更新dom時去算diff無可厚非,但是diff了半天發現無差別,不需要更新就白瞎了,O(n)雖好,做不必要的O(n)也是一件浪費資源的事,因此需要盡可能減少diff次數,也就是減少render次數。

如何減少render的次數

  1. 減少setState的次數

需要說明的是setState可以是批量的,可能多次setState只會有一次render; 也可以是setState一次就render一次。

  • 批量:在合成事件, 生命周期函數中的setState是異步批量更新的, 多次setState只會走一次render
  • 非批量:在setTimeOut, setInterval, 原生事件, Promise中的setState是同步逐個更新的, 而且每次setState都會走一次render

因此減少setState次數需要減少的是非批量的情形,在合成事件,比如onClick里邊去多次setState是沒有關系的。

舉個例子:

通常寫一個上拉加載的列表會像下邊這樣,每次請求開始時將狀態置為加載中。但是可能進頁面其實就是加載中的狀態了。

 getList(pageNum) {
    this.setState({
      status: 1, // 1 loading, 2 success, 3 error
    });
    fetchList(pageNum)
      .then(res => {
        this.setState({
          data: [...this.statedata, ...res],
        });
      })
      .catch(e => {
        console.log(e);
      });
  }

這樣存在的一個問題是第一頁會多一次不必要的setState, 因為初始狀態一般就是加載中,當你使用Component而且沒有重寫shouldComponentUpdate時,這會導致一次不必要的render的。改成下邊這樣就ok了。

  getList(pageNum) {
    // 加上這樣一個判斷
    if (this.state.status !== 1) {
      this.setState({
        status: 1,
      });
    }
    fetchList(pageNum)
      .then(res => {
        this.setState({
          data: [...this.state.data, ...res],
        });
      })
      .catch(e => {
        console.log(e);
      });
  }

第一段代碼首屏會有3次render, 第二段代碼只有兩次。

  1. 使用PureComponent

Component是每次setState都會去render, 即使你setState的值和之前相同,也會render。

PureComponent重寫了shouldComponentUpdate,在里邊做了一次淺比較,如果setState后新state和舊state相同是不會走render的。

潛在的問題是當你把state里一個對象的某個屬性值改了,由於淺比較是相等的,所以不會走render, 造成顯示異常,這個注意下就行。

舉個例子

還是上邊這個,使用會render 3次的方案,但是使用PureComponent。

class App extends React.PureComponent{}

可以發現也只render了兩次。這是因為我們的那次多余的setState set的是和原來相同的值,淺比較相等,所以沒有render。

  1. 重寫shouldComponentUpdate

這里有一個react生命周期的圖, 從圖中可以看出更新的時候每次render之前都會走shouldComponentUpdate, 當shouldComponentUpdate返回false的時候就不會render了,因此我們可以在shouldComponentUpdate中合理控制是否render.

同上,使用會render 3次的方案。

重寫一下shouldComponentUpdate, 也來做一個淺比較。

  shouldComponentUpdate(nextProps, nextState) {
    for (let key in nextState) {
      if (nextState[key] !== this.state[key]) {
        return true;
      }
    }
    for (let key in nextProps) {
      if (nextProps[key] !== this.props[key]) {
        return true;
      }
    }
    return false;
  }

發現這樣做后也只render了2次。

  1. 減少
    diff代價(這個不是減少次數的)

將state的盡量放在更小的組件中,這樣render時計算diff的成本更小一些.

舉個例子

import React from 'react';
import { Text, Button, View } from 'react-native';

class SubComponent extends React.Component {
  constructor(props, context) {
    super(props, context);
  }

  state = {
    text: 'A',
  };

  onPress = () => {
    this.setState({
      text: 'B',
    });
  };

  render() {
    console.log('SubComponent render');
    return <Button title={this.state.text} onPress={this.onPress} />;
  }
}

class SubComponent2 extends React.Component {
  render() {
    console.log('SubComponent2 render');
    return (
      <Text>F</Text>
    )
  }
}


function FunctionComponent() {
  console.log('FunctionComponent excuted')
  return <Text>G</Text>
}

class App extends React.Component {
  constructor(props, context) {
    super(props, context);
  }

  state = {
    text: 'D',
  }

  onPress = () => {
    this.setState({
      text: 'E'
    })
  }

  render() {
    console.log('App render');
    return (
      <View>
        <Button title={this.state.text} onPress={this.onPress} color="red"/>
        <SubComponent />
        <SubComponent2 />
        <FunctionComponent />
      </View>
    )
  }
}

export default App;

點紅色按鈕執行結果如下

App render
App.js:20 SubComponent render
App.js:27 SubComponent2 render
App.js:36 FunctionComponent excuted

點藍色按鈕執行結果如下

SubComponent render

若父組件內狀態變更,則他和他所有子組件都會render, 而子組件的變更則不會影響到父組件和兄弟組件。因此在實際開發中應該盡量減小state的作用范圍,如果一個狀態能收斂到組件內部,就不應該放在外邊。這樣可以降低render操作的代價。

動畫優化

  1. 啟用原生動畫渲染

Animated的 API 是可序列化的(即可轉化為字符串表達以便通信或存儲)。通過啟用原生驅動,我們在啟動動畫前就把其所有配置信息都發送到原生端,利用原生代碼在 UI 線程執行動畫,而不用每一幀都在兩端間來回溝通。如此一來,動畫一開始就完全脫離了 JS 線程,因此此時即便 JS 線程被卡住,也不會影響到動畫了。(來自reactnative中文文檔)

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

setNativeProps方法可以使我們直接修改基於原生視圖的組件的屬性,而不需要使用setState來重新渲染整個組件樹。

如果我們要更新的組件有一個非常深的內嵌結構,並且沒有使用shouldComponentUpdate來優化,那么使用setNativeProps就將大有裨益。
(來自reactnative中文文檔)

  1. InteractionManager

Interactionmanager 可以將一些耗時較長的工作安排到所有互動或動畫完成之后再進行。這樣可以保證 JavaScript 動畫的流暢運行。

如果動畫執行期間可能有比較耗時的代碼可以如下操作

InteractionManager.runAfterInteractions(() => {
  // 把耗時比較多的代碼放到這里,防止他們影響動畫效果
});

過渡繪制優化

過度繪制(Overdraw)描述的是屏幕上的某個像素在同一幀的時間內被繪制了多次。在多層次重疊的 UI 結構里面,如果不可見的 UI 也在做繪制的操作,會導致某些像素區域被繪制了多次,同時也會浪費大量的 CPU 以及 GPU 資源。

在rn應用開發中解決過度繪制問題方法如下:

子組件能將父組件占滿的情況下不要父組件背景色,而是指定子組件背景色

這里可以看個例子來了解為什么會這樣

import React from 'react';
import { Text, View, StyleSheet } from 'react-native';

const styles = StyleSheet.create({
  bg: {
    backgroundColor: '#aaa',
  },
  text: {
    padding: 30,
  }
})

export default class App extends React.PureComponent {

  render() {
    return (
      <View>
        <Text style={styles.text}>0次</Text>
        <View style={styles.bg}>
          <Text style={styles.text}>1次</Text>
          <View style={styles.bg}>
            <Text style={styles.text}>2次</Text>
            <View style={styles.bg}>
              <Text style={styles.text}>3次</Text>
              <View style={styles.bg}>
                <Text style={styles.text}>4次</Text>
              </View>
            </View>
          </View>
        </View>
      </View>
    );
  }
}

這個例子中我們不斷的疊加顏色,這樣就會產生過度繪制問題,我們可以在開發者選項開啟"顯示過度繪制"查看效果,如下

過度繪制演示

從這個例子可以看出我們應該盡量減少不必要的背景疊加來減少過度繪制。

小結

本文的優化方法基本都是從前輩大佬文章借鑒來的,部分方法做了一些小測試,大多沒有。有一些方法也是適用react web應用的。

寫的比較粗,后邊可能會對這些優化點逐一測試,做一些數據上的支撐,實實在在看看這些方法對性能有何種提升。使用的工具應該會是騰訊的性能狗, 非常容易用,大家感興趣也可以先測測。

參考文章


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM