本文是【React Native 性能優化指南】的一部分內容,因為內容比較具有代表性,所以單獨拿出進行講解;若想獲得完整優化建議,可點擊原文查看。
在 React Native 開發中,最容易遇到的對性能有一定要求場景就是長列表了。在日常業務實踐中,優化做好后,千條數據渲染還是沒啥問題的。
虛擬列表前端一直是個經典的話題,核心思想也很簡單:只渲染當前展示和即將展示的 View,距離遠的 View 用空白 View 展示,從而減少長列表的內存占用。
在 React Native 官網上,🔗 列表配置優化其實說的很好了,我們基本上只要了解清楚幾個配置項,然后靈活配置就好。但是問題就出在「了解清楚」這四個字上,本節我會結合圖文,給大家講述清楚這幾個配置。
1⃣️ 各種列表間的關系
React Native 有好幾個列表組件,先簡單介紹一下:
- ScrollView:會把視圖里的所有 View 渲染,直接對接 Native 的滾動列表
- VirtualizedList:虛擬列表核心文件,使用 ScrollView,長列表優化配置項主要是控制它
- FlatList:使用 VirtualizedList,實現了一行多列的功能,大部分功能都是 VirtualizedList 提供的
- SectionList:使用 VirtualizedList,底層使用 VirtualizedSectionList,把二維數據轉為一維數據
還有一些其他依賴文件,有個🔗 博文的圖總結的挺好的,我這里借用它的圖一下:
我們可以看出 VirtualizedList 才是主演,下面我們結合一些示例代碼,分析它的配置項。
2⃣️ 列表配置項
講之前先寫個小 demo。demo 非常簡單,一個基於 FlatList 的奇偶行顏色不同的列表。
export default class App extends React.Component {
renderItem = item => {
return (
<Text
style={{
backgroundColor: item.index % 2 === 0 ? 'green' : 'blue',
}}>
{'第 ' + (item.index + 1) + ' 個'}
</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} // 首批渲染的元素數量
windowSize={3} // 渲染區域高度
removeClippedSubviews={Platform.OS === 'android'} // 是否裁剪子視圖
maxToRenderPerBatch={10} // 增量渲染最大數量
updateCellsBatchingPeriod={50} // 增量渲染時間間隔
debug // 開啟 debug 模式
/>
</View>
);
}
}
VirtualizedList 有個 debug 的配置項,開啟后會在視圖右側顯示虛擬列表的顯示情況。
這個屬性文檔中沒有說,是翻🔗 源碼發現的,我發現開啟它后用來演示講解還是很方便的,可以很直觀的學習 initialNumToRender、windowSize、Viewport,Blank areas 等概念。
下面是開啟 debug 后的 demo 截屏:
上面的圖還是很清晰的,右側 debug 指示條的黃色部分表示內存中 Item,各個屬性我們再用文字描述一下:
1.initialNumToRender
首批應該渲染的元素數量,剛剛蓋住首屏最好。而且從 debug 指示條可以看出,這批元素會一直存在於內存中。
2.Viewport
視口高度,就是用戶能看到內容,一般就是設備高度。
3.windowSize
渲染區域高度,一般為 Viewport 的整數倍。這里我設置為 3,從 debug 指示條可以看出,它的高度是 Viewport 的 3 倍,上面擴展 1 個屏幕高度,下面擴展 1 個屏幕高度。在這個區域里的內容都會保存在內存里。
將 windowSize 設置為一個較小值,能有減小內存消耗並提高性能,但是快速滾動列表時,遇到未渲染的內容的幾率會增大,會看到占位的白色 View。大家可以把 windowSize 設為 1 測試一下,100% 會看到占位 View。
4.Blank areas
空白 View,VirtualizedList 會把渲染區域外的 Item 替換為一個空白 View,用來減少長列表的內存占用。頂部和底部都可以有。
上圖是渲染圖,我們可以利用 react-devtools 再看看 React 的 Virtual DOM(為了截屏方便,我把 initialNumToRender 和 windowSize 設為 1),可以看出和上面的示意圖是一致的。
5.removeClippedSubviews
這個翻譯過來叫「裁剪子視圖」的屬性,文檔描述不是很清晰,大意是設為 true 可以提高渲染速度,但是 iOS 上可能會出現 bug。這個屬性 VirtualizedList 沒有做任何優化,是直接透傳給 ScrollView 的。
在 0.59 版本的一次 🔗 commit 里,FlatList 默認 Android 開啟此功能,如果你的版本低於 0.59,可以用以下方式開啟:
removeClippedSubviews={Platform.OS === 'android'}
6.maxToRenderPerBatch 和 updateCellsBatchingPeriod
VirtualizedList 的數據不是一下子全部渲染的,而是分批次渲染的。這兩個屬性就是控制增量渲染的。
這兩個屬性一般是配合着用的,maxToRenderPerBatch 表示每次增量渲染的最大數量,updateCellsBatchingPeriod 表示每次增量渲染的時間間隔。
我們可以調節這兩個參數來平衡渲染速度和響應速度。但是,調參作為一門玄學,很難得出一個統一的「最佳實踐」,所以我們在業務中也沒有動過這兩個屬性,直接用的系統默認值。
2⃣️ ListLtems 優化
📄 ListLtems 優化 文檔:https://reactnative.cn/docs/optimizing-flatlist-configuration/#list-items
文檔中說了好幾點優化,其實在前文我都介紹過了,這里再簡單提一下:
1.使用 getItemLayout
如果 FlatList(VirtualizedList)的 ListLtem 高度是固定的,那么使用 getItemLayout 就非常的合算。
在源碼中(#L1287、#L2046),如果不使用 getItemLayout,那么所有的 Cell 的高度,都要調用 View 的 onLayout 動態計算高度,這個運算是需要消耗時間的;如果我們使用了 getItemLayout,VirtualizedList 就直接知道了 Cell 的高度和偏移量,省去了計算,節省了這部分的開銷。
在這里我還想提一下幾個注意點,希望大家使用 getItemLayout 要多注意一下:
- 如果 ListItem 高度不固定,使用 getItemLayout 返回固定高度時,因為最終渲染高度和預測高度不一致,會出現頁面跳動的問題【🔗 問題鏈接】
- 如果使用了
ItemSeparatorComponent
,分隔線的尺寸也要考慮到 offset 的計算中【🔗 文檔鏈接】 - 如果 FlatList 使用的時候使用了
ListHeaderComponent
,也要把 Header 的尺寸考慮到 offset 的計算中【🔗 官方示例代碼鏈接】
2.Use simple components & Use light components
使用簡單組件,核心就是減少邏輯判斷和嵌套,優化方式可以參考「二、減輕渲染壓力」的內容。
3.Use shouldComponentUpdate
參考「一、re-render」的內容。
4.Use cached optimized images
參考「三、圖片優化那些事」的內容。
5.Use keyExtractor or key
常規優化點了,可以看 React 的文檔 🔗 列表 & Key。
6.Avoid anonymous function on renderItem
renderItem 避免使用匿名函數,參考「四、對象創建調用分離」的內容。
最后推薦一下我的個人公眾號:「鹵蛋實驗室」,平時會分享一些前端技術和數據分析的內容,大家感興趣的話可以關注一波: