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