本文將簡單介紹一下我所收集到的React Native應用優化方法,希望對你有所啟發。很多方法也是適用React web應用的。 ...
本文將簡單介紹一下我所收集到的React Native應用優化方法,希望對你有所啟發。很多方法也是適用React web應用的。
包體積優化
無論是熱更新方案走網路下載js,還是直接將js打進apk,減小js bundle體積都很必要。
走網路的js體積大影響首次載入速度,打進apk的增加包體積。
- 壓縮
為了測試,直接使用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.
- 包拆分
上邊可以看到一個空工程打包出來的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的次數
- 減少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, 第二段代碼只有兩次。
- 使用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。
- 重寫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次。
- 減少
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操作的代價。
動畫優化
- 啟用原生動畫渲染
Animated的 API 是可序列化的(即可轉化為字元串表達以便通信或存儲)。通過啟用原生驅動,我們在啟動動畫前就把其所有配置信息都發送到原生端,利用原生代碼在 UI 線程執行動畫,而不用每一幀都在兩端間來回溝通。如此一來,動畫一開始就完全脫離了 JS 線程,因此此時即便 JS 線程被卡住,也不會影響到動畫了。(來自reactnative中文文檔)
Animated.timing(this.state.animatedValue, {
toValue: 1,
duration: 500,
useNativeDriver: true // <-- 加上這一行
}).start();
- setNativeProps
setNativeProps方法可以使我們直接修改基於原生視圖的組件的屬性,而不需要使用setState來重新渲染整個組件樹。
如果我們要更新的組件有一個非常深的內嵌結構,並且沒有使用shouldComponentUpdate來優化,那麼使用setNativeProps就將大有裨益。
(來自reactnative中文文檔)
- 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應用的。
寫的比較粗,後邊可能會對這些優化點逐一測試,做一些數據上的支撐,實實在在看看這些方法對性能有何種提升。使用的工具應該會是騰訊的性能狗, 非常容易用,大家感興趣也可以先測測。