如何提高 React Native FlatList 性能

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í)慣使用 @DylanVannreact-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}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容