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è)組件子樹。如要避免不必要的子組件的重渲染,有以下途徑:
- 實(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} />;
}
}
- 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])
- 如果編寫的是純粹的組件(界面完全由 props 和 state 所決定),可以使用PureComponent來(lái)獲得更好的性能
// 示例
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 短語(yǔ)法
render() {
return (
<>
<ChildA />
<ChildB />
<ChildC />
</>
);
}
- 減少 GPU 過(guò)度繪制
- 減少背景色的重復(fù)設(shè)置:每個(gè) View 都設(shè)置背景色的話停士,在 Android上會(huì)造成非常嚴(yán)重的過(guò)度繪制挖帘;并且只有布局屬性時(shí),React Native 還會(huì)減少 Android 的布局嵌套
- 避免設(shè)置半透明顏色:半透明色區(qū)域 iOS Android 都會(huì)引起過(guò)度繪制
- 避免設(shè)置圓角:圓角部位 iOS Android 都會(huì)引起過(guò)度繪制
- 避免設(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ù)