rn性能優(yōu)化 結(jié)合網(wǎng)上資料總結(jié)如下
1、首屏渲染問題。采用JS Bundle拆包解決。就是主體框架react單獨(dú)打成一個(gè)基礎(chǔ)包,一旦進(jìn)入app就馬上加載,而相關(guān)業(yè)務(wù)模塊單獨(dú)拆分成多個(gè)包,進(jìn)入相應(yīng)模塊才動(dòng)態(tài)加載。這樣可以大大加快APP的啟動(dòng)速度,各個(gè)業(yè)務(wù)也能獨(dú)立開發(fā),各自維護(hù)、下載、更新
2、圖片問題。rn開發(fā)時(shí)本地圖標(biāo)為了統(tǒng)一往往放在js端,極端時(shí)(如一個(gè)頁面加載幾十上百?gòu)垐D片)可能會(huì)有性能問題。這是因?yàn)槿绻Y源從 javascript 包中加載, RN 需要先從包中拿到資源,然后通過bridge把資源傳送到 原生UI 層去渲染。而如果資源已經(jīng)存在在原生端,那么 React 可以直接告知 UI 層去渲染具體的圖片,無需通過這個(gè)bridge引入或者轉(zhuǎn)入圖片資源。 當(dāng)然不會(huì)有這類問題,但是要js端圖片要注意壓縮,使其不太大,圖片越大,性能問題越容易凸顯。webP,jpg優(yōu)先
3、緩存。各種需要的,有必要的緩存,如一個(gè)生日日期選擇picker組件,數(shù)據(jù)源大概有100(年)x12(月)x30(天)這么多條數(shù)據(jù),如果每次彈出picker都需要計(jì)算這些數(shù)據(jù),還是會(huì)稍微有點(diǎn)延遲,這里可以緩存下來,甚至本地?cái)?shù)據(jù)存儲(chǔ)起來,以后拿出來直接使用
4、延遲加載。頁面打開,優(yōu)先執(zhí)行那些跟頁面展示有關(guān)的代碼,其他的如埋點(diǎn),上傳狀態(tài),gif動(dòng)畫都可以稍后執(zhí)行。對(duì)那些觸摸響應(yīng)事件后才需要展示的組件,或者根據(jù)接口返回才能決定是否展示的組件,一開始甚至都可以不用import,直到確定要展示時(shí)才局部import導(dǎo)入組件展示。對(duì)長(zhǎng)列表頁面,圖片較多時(shí),在頁面范圍之外的圖片可以先不展示,直到滾動(dòng)后發(fā)現(xiàn)圖片在屏幕上面顯示了再展示
5、動(dòng)畫。普通動(dòng)畫如移動(dòng),縮放等直接使用LayoutAnimation,性能更好。復(fù)雜點(diǎn)的動(dòng)畫才使用Animated。對(duì)幀動(dòng)畫這種需要快速更新state觸發(fā)動(dòng)畫的場(chǎng)景,可以使用setNativeProps直接修改原生屬性(某些場(chǎng)合如背景動(dòng)畫,gif圖片可能不是很好的選擇,因?yàn)間if可能會(huì)很大,導(dǎo)致初次解壓時(shí)出現(xiàn)明顯卡頓現(xiàn)象,而且安卓上gif圖片首輪顯示效果不佳)。
Animated: useNativeDriver為true,則會(huì)一次性將動(dòng)畫信息發(fā)送給原生端讓原生去驅(qū)動(dòng)動(dòng)畫,性能更佳。 否則js端會(huì)不斷注冊(cè)定時(shí)器事件,讓原生端不斷回調(diào)js方法更改組件的setNativeProps值產(chǎn)生動(dòng)畫,因?yàn)閯?dòng)畫配置信息在每一幀都在原生和js端通信性能有所損耗,
問題: 為什么不總是使用useNativeDriver? 是因?yàn)橛行﹦?dòng)畫原生不支持么?
6、響應(yīng)速度。由于js是單線程,當(dāng)在執(zhí)行一些計(jì)算量很大的任務(wù)時(shí)可能會(huì)造成堵塞卡頓現(xiàn)象。此時(shí)可以將任務(wù)稍微延后執(zhí)行,避免大量任務(wù)在同一個(gè)js 事件循環(huán)中導(dǎo)致其他任務(wù)無法執(zhí)行。相應(yīng)的方法有InteractionManager,requestAnimationFrame,setTimeOut(0)等,原理都大同小異
7、刷新問題。每次setState導(dǎo)致的render都會(huì)進(jìn)行一次內(nèi)存中diff計(jì)算,盡管diff效率很高(O(n)),但是還是應(yīng)該避免不必要的diff。 Pure組件、自定義shouldComponentUpdate實(shí)現(xiàn)避免不必要的刷新
8、預(yù)加載。對(duì)一些重要的,很可能會(huì)用到的內(nèi)容預(yù)先加載,例如圖片瀏覽器,當(dāng)瀏覽某一張圖片時(shí)可以預(yù)加載前后兩張圖片,優(yōu)化用戶體驗(yàn)。
9、FlatList的優(yōu)化。
頁面中的重頭戲FlatList,盡管經(jīng)過了大量?jī)?yōu)化,在數(shù)據(jù)較多時(shí)使用還是需要注意的。
FlatList的頻繁刷新問題很常見,如下面
class FlatListTest extends React.Component {
state = {
index: 1,
data: []
}
componentDidMount() {
let data = [];
for (let index = 0; index < 100; index++) {
data.push(index);
}
this.setState({ data })
}
renderItem = (item) => {
console.log('表格刷新了');
return (
<View style={{ height: 50 }}>
<Text>
{item.item}
</Text>
</View>
)
}
render() {
console.log('頁面刷新了');
return (
<View>
<FlatList
style={{ width: SCREEN_W, height: 444 }}
data={this.state.data}
keyExtractor={(_, index) => index + ''}
renderItem={this.renderItem}
ListFooterComponent={<View style={{ width: 100, height: 20, backgroundColor: 'red' }} />}
/>
<TouchableWithoutFeedback onPress={() => this.setState({ index: this.state.index + 1 })}>
<View style={{ width: 100, height: 100, backgroundColor: 'red' }}></View>
</TouchableWithoutFeedback>
</View>
)
}
}
這樣子寫FlatList看起來沒什么問題,但是性能上完全具有優(yōu)化空間
點(diǎn)擊下方紅色按鈕讓index累加,頁面會(huì)刷新,但是也會(huì)導(dǎo)致FlatList刷新,renderItem被調(diào)用98次,就是說頁面刷新->表格刷新->所有的表格cell也會(huì)刷新。很顯然當(dāng)有大量cell時(shí)容易造成性能問題。
FlatList是一個(gè)PureComponment,只會(huì)對(duì)傳入的屬性進(jìn)行淺比較(對(duì)象地址比較),發(fā)現(xiàn)不一樣就會(huì)刷新。
例子中,F(xiàn)latList的style,keyExtractor,ListFooterComponent這三個(gè)地方傳入的對(duì)象在頁面刷新時(shí)會(huì)重新生成,導(dǎo)致傳入FlatList的屬性地址發(fā)生變化,F(xiàn)latList刷新??梢圆捎孟旅娴姆绞叫迯?fù)。
renderFooter = () => {
return <View style={{ width: 100, height: 20, backgroundColor: 'red' }} />
}
keyExtractor = (_, index) => {
return index + ''
}
getItemLayout = (_, index) => {
return { length: 50, offset: 50 * index, index }
}
render() {
console.log('頁面刷新了');
return (
<View>
<FlatList
style={styles.flatStyle}
data={this.state.data}
keyExtractor={this.keyExtractor}
renderItem={this.renderItem}
ListFooterComponent={this.renderFooter}
getItemLayout={this.getItemLayout}
/>
<TouchableWithoutFeedback onPress={() => this.setState({ index: this.state.index + 1 })}>
<View style={{ width: 100, height: 100, backgroundColor: 'red' }}></View>
</TouchableWithoutFeedback>
</View>
)
}
const styles = StyleSheet.create({
flatStyle: { width: SCREEN_W, height: 444 }
});
原則就是確保頁面刷新后,傳入FlatList的所有對(duì)象地址不發(fā)生變化,這樣就不會(huì)導(dǎo)致不必要的刷新。
getItemLayout的設(shè)置也比較重要,設(shè)置后,則列表滾動(dòng)時(shí),新出現(xiàn)cell時(shí)就不用動(dòng)態(tài)去測(cè)量cell高度,可以直接從這里拿到,優(yōu)化性能
把上面的實(shí)現(xiàn)改成Hook,會(huì)發(fā)現(xiàn)頁面刷新又會(huì)導(dǎo)致表格刷新,因?yàn)镠ook組件每次刷新時(shí)內(nèi)部的函數(shù)都會(huì)被重新定義,也就是函數(shù)地址發(fā)生了變化,從而導(dǎo)致FlatList的刷新。這里需要使用useCallback將所有函數(shù)都緩存好,避免函數(shù)組件刷新導(dǎo)致函數(shù)從新被定義,如下這樣,注意依賴
const renderItem = useCallback((item) => {
console.log('表格刷新了');
return (
<View style={{ height: 50 }}>
<Text>
{item.item}
</Text>
</View>
)
}, [])
本人項(xiàng)目中有個(gè)類似微信朋友圈的列表,當(dāng)數(shù)據(jù)很多時(shí),在debug環(huán)境下點(diǎn)擊圖片瀏覽時(shí)稍微會(huì)有卡頓現(xiàn)象。糾其原因就是因?yàn)辄c(diǎn)擊圖片瀏覽觸發(fā)頁面刷新,所有cell跟著刷新完成后才會(huì)顯示大圖導(dǎo)致卡頓,用如上優(yōu)化后就ok了。
其他:
FlatList顯示規(guī)則是,在ScrollView上面添加View,只渲染當(dāng)前展示和即將展示的 View,距離遠(yuǎn)的 View 用空白 View 展示,從而減少長(zhǎng)列表的內(nèi)存占用。
FlatList的item無法復(fù)用,目前了解到的是跟js單線程有關(guān),具體不太明白
重要屬性:
getItemLayout,如果不使用,那么所有的 Cell 的高度,都要調(diào)用 View 的 onLayout 動(dòng)態(tài)計(jì)算高度,這個(gè)運(yùn)算是需要消耗時(shí)間的;
為什么需要?jiǎng)討B(tài)計(jì)算每一個(gè)View高度? 想一想如果不測(cè)量,那么原生端View的Frame如何設(shè)置就可以理解了。
windowSize: 表征緩存屏幕外的item多少,單位是一個(gè)屏幕顯示的item數(shù)量。默認(rèn)為21。例如一個(gè)屏幕能顯示8個(gè)item,那么默認(rèn)情況下,屏幕上下各緩存10*8個(gè)item, 減少該數(shù)字能減小內(nèi)存消耗并提高性能,但是快速滾動(dòng)列表時(shí),遇到未渲染的空白view幾率增大。這里要注意,因?yàn)橹挥挟?dāng)列表停止?jié)L動(dòng)時(shí)才會(huì)更新渲染區(qū)域,所以只要item足夠多,一直滾動(dòng)不要停止就一定能看到空白view。
maxToRenderPerBatch: 每批次渲染的item個(gè)數(shù),默認(rèn)為10. 例如一個(gè)屏幕能顯示8個(gè)item, 列表停止時(shí)默認(rèn)情況下需要緩存屏幕上下各80個(gè)item, 那么需要16個(gè)批次才能完成,如果列表停留時(shí)間不夠用戶馬上又繼續(xù)滾動(dòng),因?yàn)榇藭r(shí)緩存的item數(shù)量還不夠,可能出現(xiàn)滾不動(dòng)的現(xiàn)象。 如果該值變大則會(huì)使所需批次減少,緩存足夠item所需時(shí)間減小,用戶體驗(yàn)更好。 但是如此js一個(gè)事件循環(huán)任務(wù)過多可能導(dǎo)致其他的如列表響應(yīng)問題。 有時(shí)候設(shè)置該值是必要的,比如一個(gè)長(zhǎng)列表,每屏幕能顯示下20個(gè)item,那么默認(rèn)情況maxToRenderPerBatch為10就顯得太小,滑動(dòng)時(shí)很容易出現(xiàn)滑不動(dòng)現(xiàn)象,可以適當(dāng)放大該值。
removeClippedSubviews: 剪切子視圖,移除屏幕外較遠(yuǎn)位置的所有item,優(yōu)化內(nèi)存。iOS上面有bug,安卓默認(rèn)開啟。 主要是在ListView時(shí)期長(zhǎng)列表優(yōu)化內(nèi)存使用。
10、hook自定義組件
例如我項(xiàng)目中自定義了個(gè)button組件
export const Button = memo((props) => {
let children = props.children;
let { disabled, loading, style, onPress } = props;
if (typeof children == 'string') {
children = <Text style={{ color: 'white', fontSize: 18 }}>{children}</Text>
}
let defaultStyle = {
height: 45, marginLeft: 15, marginRight: 15, alignItems: 'center', justifyContent: 'center',
backgroundColor: ColorConf.main(), borderRadius: 5, opacity: loading || disabled ? 0.5 : 1, flexDirection: 'row'
}
if (style) {
defaultStyle = { ...defaultStyle, ...style }
}
return (
<TouchableWithoutFeedback onPress={() => !disabled && onPress && onPress()}>
<View style={defaultStyle}>
{loading ? <ActivityIndicator animating={true} color='white' style={{ marginRight: 8 }} /> : null}
{children}
</View>
</TouchableWithoutFeedback>
)
})
用memo包裹起來跟class時(shí)代的pure組件差不多,每次會(huì)對(duì)傳入的props進(jìn)行淺比較,若不一致才會(huì)更新組件
<Button
disabled={!(name && password)}
loading={logining}
style={{ marginTop: 50 }}
onPress={_loginInWithPassword} >
Login In
</Button>
如果像這樣使用,那么每當(dāng)父組件刷新時(shí),由于傳入Button的style是一個(gè)臨時(shí)對(duì)象,Button會(huì)隨著父組件一同刷新,顯然是不合適的
同上面,應(yīng)該如下使用
const _loginInWithPassword = useCallback(() => console.log('點(diǎn)擊登陸') },[])
<Button
disabled={!(name && password)}
loading={logining}
style={styles. buttonStyle}
onPress={_loginInWithPassword} >
Login In
</Button>
const styles = StyleSheet.create({
buttonStyle: { marginTop: 50 }
});
使用useCallback,useMemo等緩存函數(shù),組件等的時(shí)候要注意設(shè)置好依賴,否則可能出現(xiàn)值捕獲等隱性問題
11、使用Fragment
Fragment和View都可以包裹子元素,但是前者不對(duì)應(yīng)具體的視圖,僅僅是代表可以包裝而已,跟空的標(biāo)識(shí)符一樣
<React.Fragment>
<ChildA />
<ChildB />
</React.Fragment>
<>
<ChildA />
<ChildB />
</>
<View>
<ChildA />
<ChildB />
</View >
如上,前面兩個(gè)完全一樣,原生端只存在ChildA和ChildB兩個(gè)組件。最后那個(gè)不一致,對(duì)應(yīng)原生端為View父視圖包含ChildA和ChildB兩個(gè)個(gè)組件
視圖層級(jí)關(guān)系減少有利于視圖渲染