React Native 的 FlatList 性能在大多數(shù)情況下是很棒的。但是有時(shí)候如果數(shù)據(jù)量比較大時(shí)它也存在一些缺陷。我們可以在網(wǎng)絡(luò)上找到很多 issues 和博客文章講述如何提高它的性能。在這里,我試圖整理一份關(guān)于此問題相對(duì)想盡的文檔。
事先聲明,沒有所謂的可以解決所有問題的「銀彈」。在實(shí)際應(yīng)用場(chǎng)景中,我們?nèi)孕杷伎寄姆N方式和解決方案最適合我們的業(yè)務(wù)需求。
一些關(guān)系和概念
一開始使用 FlatList 相關(guān)組件進(jìn)行開發(fā)的時(shí)候,有很多概念讓我困惑。所以讓我們先將他們來梳理一下:
-
VirtualizedList 是 FlatList 的底層實(shí)現(xiàn),直接使用 FlatList 和 SectionList 會(huì)更加方便。一般來說,僅當(dāng)想獲得比 FlatList 更高的靈活性(比如說在使用 immutable data 而不是 普通數(shù)組)的時(shí)候,你才應(yīng)該考慮使用 VirtualizedList。具體可以查看官方文檔的
Virtualizedlist部分。也可以參考Virtualized List 這個(gè)組件的實(shí)現(xiàn),它提供 React 網(wǎng)頁版本的列表實(shí)現(xiàn),支持顯示大量數(shù)據(jù),針對(duì)數(shù)據(jù)顯示的列表內(nèi)組件自動(dòng)回收,擁有出色的性能。 - Performance 性能 此文中,我們主要指列表性能,即為用戶提供順滑的滾動(dòng)瀏覽內(nèi)容的體驗(yàn)。
- Memory consumption 內(nèi)存消耗 列表數(shù)據(jù)在內(nèi)存中占用多大,這關(guān)系到 APP 的穩(wěn)定性,如果占用內(nèi)存過大,可能導(dǎo)致你的應(yīng)用崩潰(被操作系統(tǒng)終止)。
- Responsiveness 響應(yīng)能力 這里指應(yīng)用對(duì)交互的響應(yīng)能力的快慢。舉個(gè)例子,如果用戶點(diǎn)擊列表中的某一項(xiàng)之后要彈出提示或者跳轉(zhuǎn)頁面,如果跳轉(zhuǎn)頁面很慢的話,我們稱之為響應(yīng)能力比較弱。
- Blank areas 空白區(qū)域 如果列表來不及渲染具體的列表項(xiàng)(如快速滾動(dòng)列表的過程中),你將看到列表的背景,也就是一片白色。
- Window 視窗 這里指列表內(nèi)容的可視化區(qū)域。
Props(組件屬性)
調(diào)整和優(yōu)化組件屬性是一種可行的優(yōu)化 FlatList 性能的方式。以下是幾種關(guān)于這種思路的調(diào)整方式。
設(shè)定 removeClippedSubviews 屬性
你可以設(shè)定 removeClippedSubviews 屬性為 true(默認(rèn)為 false),如果列表項(xiàng)組件滾動(dòng)到列表的可視區(qū)域之外會(huì)自動(dòng)卸載(unmount)。
優(yōu)點(diǎn): 這種方式對(duì)內(nèi)存占用比較友好,F(xiàn)latList 將僅渲染一部分列表項(xiàng),而非渲染所有數(shù)據(jù),類似 iOS 中的 TableView。
缺點(diǎn):如果遇到頻繁、高速的滾動(dòng),會(huì)導(dǎo)致大量的列表項(xiàng)組件初始化和卸載動(dòng)作,雖然內(nèi)存占用被減少,但隨之而來的是計(jì)算量的大量增加,對(duì)于一些性能不夠高的設(shè)備來說會(huì)出現(xiàn)明顯的卡頓現(xiàn)象,影響用戶體驗(yàn)。如果列表項(xiàng)組件內(nèi)包含一些復(fù)雜的初始化操作和數(shù)據(jù)引用,可能會(huì)導(dǎo)致一些問題或內(nèi)存泄漏。根據(jù)官方文檔標(biāo)注,開啟此設(shè)定后在有些情況下會(huì)有 bug(比如內(nèi)容無法顯示),需謹(jǐn)慎使用。(嘗試設(shè)定 removeClippedSubviews 為 true 后,測(cè)試一個(gè) 50 項(xiàng)列表的渲染,頻繁滾動(dòng)它,可以看到 CPU 占用有 5-10% 的上浮)
maxToRenderPerBatch
可參考官方的 VisualizedList API 說明,摘要如下:每批增量渲染可渲染的最大數(shù)量。能立即渲染出的元素?cái)?shù)量越多,填充速率就越快,但是響應(yīng)性可能會(huì)有一些損失,因?yàn)槊總€(gè)被渲染的元素都可能參與或干擾對(duì)按鈕點(diǎn)擊事件或其他事件的響應(yīng)。這個(gè)屬性默認(rèn)數(shù)值是 10。
優(yōu)點(diǎn): 如果我們?cè)O(shè)定一個(gè)比較大的數(shù)值,每批渲染的列表項(xiàng)比較多,這樣在滾動(dòng)的時(shí)候會(huì)更小概率出現(xiàn)空白區(qū)域(未及時(shí)渲染)顯示。
缺點(diǎn):參考文檔我們可以了解到新的一批列表項(xiàng)在滾動(dòng)到可視區(qū)域前就開始異步渲染,每批數(shù)量越大,所需要的計(jì)算量越大。這可能會(huì)阻塞 JS 線程,導(dǎo)致應(yīng)用程序無法快速響應(yīng)用戶的點(diǎn)擊事件。如果實(shí)際應(yīng)用場(chǎng)景中需要渲染的列表是靜態(tài)的、不需要響應(yīng)用戶交互的,將此屬性數(shù)值調(diào)大是一個(gè)不錯(cuò)的選擇。
updateCellsBatchingPeriod
用于具有較低渲染優(yōu)先級(jí)的元素(比如那些離屏幕相當(dāng)遠(yuǎn)的元素)的渲染批次之間的時(shí)間間隔。與 maxToRenderPerBatch 具有相同的目的,都是為了在渲染速率和響應(yīng)性之間獲得一個(gè)平衡。
這個(gè)屬性的單位是毫秒,默認(rèn)值是 50 毫秒。
優(yōu)點(diǎn): 可以與 maxToRenderPerBatch 屬性配合來調(diào)整渲染的節(jié)奏,找到更適合你應(yīng)用場(chǎng)景的方式。
缺點(diǎn):性能和體驗(yàn)的平衡是很難平衡的,性能和視覺體驗(yàn)的抉擇是有點(diǎn)困難的。
initialNumToRender
設(shè)定列表組件初始化渲染時(shí)默認(rèn)渲染的列表項(xiàng)數(shù)量,默認(rèn)為 10。如果每一個(gè)列表項(xiàng)的高度比較大,也許數(shù)值設(shè)定的更小會(huì)更加合適。當(dāng)然,需要填充可視區(qū)域,否則用戶將看到空白區(qū)域。
windowSize
設(shè)置可視區(qū)外最大能被渲染的元素的數(shù)量,以可視區(qū)的長(zhǎng)度為單位。比如說,如果列表占滿了整個(gè)屏幕,而 windowSize 屬性被設(shè)置為 21 的話,那渲染的長(zhǎng)度為包括當(dāng)前可見屏幕區(qū)域在內(nèi),往上 10 個(gè)屏幕的長(zhǎng)度和往下 10 個(gè)屏幕的長(zhǎng)度。將 windowSize 設(shè)置為一個(gè)較小值,能有減小內(nèi)存消耗并提高性能,但是當(dāng)你快速滾動(dòng)列表時(shí),遇到尚未渲染的內(nèi)容的幾率會(huì)增大,而這些尚未渲染的內(nèi)容會(huì)暫時(shí)性地被空白區(qū)塊所替代。
優(yōu)點(diǎn): 在性能允許的情況下,可以設(shè)定一個(gè)大的數(shù)值來保證更加流暢的用戶體驗(yàn),并避免空白區(qū)域顯示,比如應(yīng)用程序僅需要支持幾款比較新的特定設(shè)備。如果考慮兼容更多的設(shè)備(比如已經(jīng)發(fā)布了很久的機(jī)型),建議盡量將此數(shù)值調(diào)低一些。
缺點(diǎn):如果將數(shù)值調(diào)大,將會(huì)導(dǎo)致更多的內(nèi)存占用,這對(duì)一部分設(shè)備來說會(huì)比較吃力。如果數(shù)值設(shè)置的太小,會(huì)導(dǎo)致更高頻的計(jì)算。
legacyImplementation
如果此屬性設(shè)定為 true,將基于 ListView進(jìn)行實(shí)現(xiàn)。默認(rèn)值為 false。
優(yōu)點(diǎn): 一次性渲染列表所有項(xiàng),沒有 VisualizationList組件實(shí)現(xiàn)方式帶來額外計(jì)算開銷,用戶滾動(dòng)列表將變得非常流暢。
缺點(diǎn):如果列表內(nèi)容很多,將占用很多內(nèi)存空間,觸發(fā)內(nèi)存警告,甚至?xí)?dǎo)致應(yīng)用內(nèi)存占用過大被系統(tǒng)殺死。
disableVirtualization
這個(gè)屬性已經(jīng)被取消了,我們這里就不講了。
List items
除了優(yōu)化列表屬性,也有一些列表項(xiàng)渲染的優(yōu)化策略。
讓列表項(xiàng)組件邏輯盡可能簡(jiǎn)單
列表項(xiàng)組件越復(fù)雜,渲染越慢。我們需要盡可能的避免在列表項(xiàng)組件中編寫復(fù)雜的業(yè)務(wù)邏輯,如果你的列表項(xiàng)組件在應(yīng)用多處復(fù)用,建議盡量為數(shù)據(jù)量較大的列表創(chuàng)建一個(gè)單獨(dú)的、簡(jiǎn)化邏輯的版本來單獨(dú)使用。
讓組件盡可能變得輕量
避免在列表項(xiàng)組件中加載過多的圖片等資源。建議與設(shè)計(jì)團(tuán)隊(duì)溝通,盡可能的減少列表項(xiàng)的交互,通過跳轉(zhuǎn)或其他方式來實(shí)現(xiàn)更多交互。
使用 shouldComponentUpdate 生命周期
為列表項(xiàng)組件增加更新判斷邏輯。React 的 PureComponent 默認(rèn)通過數(shù)據(jù)的淺比較來實(shí)現(xiàn) shouldComponentupdate。由于這種實(shí)現(xiàn)方式需要檢查你的每一個(gè)屬性,代價(jià)是比較高的。如果我們需要實(shí)現(xiàn)更好的性能,我們需要為列表項(xiàng)組件創(chuàng)建嚴(yán)格的屬性規(guī)則,僅檢查可能出現(xiàn)變動(dòng)的特定屬性。如果你的組件足夠簡(jiǎn)單,甚至可以這樣做:
shouldComponentUpdate() {
return false
}
使用經(jīng)過優(yōu)化的圖片組件
我個(gè)人習(xí)慣使用 @DylanVann 的 react-native-fast-image 組件。列表項(xiàng)組件中的每一個(gè)圖片都是一個(gè) new Image() 實(shí)例。圖片實(shí)力越早完成 loaded 狀態(tài),JavaScript 線程將越早被釋放資源。
使用 getItemLayout
getItemLayout 是一個(gè)可選的優(yōu)化,用于避免動(dòng)態(tài)測(cè)量?jī)?nèi)容尺寸的開銷,不過前提是你可以提前知道內(nèi)容的高度。如果你的行高是固定的,getItemLayout 用起來就既高效又簡(jiǎn)單。
getItemLayout = (data, index) => ({
length: 70,
offset: 70 * index,
index
})
使用 keyExtractor
此函數(shù)用于為給定的 item 生成一個(gè)不重復(fù)的 key。Key 的作用是使 React 能夠區(qū)分同類元素的不同個(gè)體,以便在刷新時(shí)能夠確定其變化的位置,減少重新渲染的開銷。若不指定此函數(shù),則默認(rèn)抽取 item.key 作為 key 值。若 item.key 也不存在,則使用數(shù)組下標(biāo)。
keyExtractor={item => item.id}