在React專案中,如何優雅的優化長列表
對於較長的列表,比如1000個數組的資料結構,如果想要同時渲染這1000個數據,生成相應的1000個原生dom,我們知道原生的dom元素是很複雜的,如果長列表通過生成如此多的dom元素來實現,很可能使網頁失去響應。
貫穿React核心的就是"virtual dom",我們同樣的可以通過用虛擬列表的方式來優雅的優化長列表
- 原生dom渲染長列表的缺陷
- 虛擬列表優化長列表的原理
- 通過react-virtualized來優化長列表
- 通過react-tiny-virtual-list來優化長列表
本文的原文地址釋出在我的部落格中:
歡迎star和fork~
本文的用例的程式碼地址為:
一、原生dom渲染長列表的缺陷
首先我們嘗試在React專案中,未做任何優化一次性渲染1000個dom,每個dom包含一個img標籤,原生dom本身是很複雜的物件,加上img標籤後。渲染的效果如下圖所示:
可以很明顯的看到白屏的時間很長,因為在React中,不做任何優化,直接渲染這麼包含1000個圖片的dom節點,即使React本身用了虛擬dom,但是在首次渲染的時候,是實實在在的生成了1000個真實的dom,我們可以檢視網頁中的真實dom情況,如下所示:從上圖我們可以看出,是確確實實的生成了1000個真實的dom,進入頁面後,需要渲染這1000個dom,因此白屏的時間很長。
此外,在直接渲染1000個dom節點的頁面,觸發滾動事件,也會使得記憶體用量增加,具體可以如下圖所示:
此外同時渲染很多dom節點,也會造成一下幾個問題:
-
容易失幀,因為渲染很慢,所以無法維持瀏覽器的幀率,主觀上會顯得頁面卡頓
-
網頁失去響應,事件等無法及時被觸發
上述的效果都是在PC端展示的,對於特定的移動裝置,直接無優化的渲染長列表所造成的問題會更加的放大。長列表的渲染在移動端的很多場景會遇到,比如微博,feeds流中等等。合理的優化長列表,可以提升使用者體驗。
二、虛擬列表優化長列表的原理
優化長列表的原理很簡單,基本原理可以一句話概括:
用陣列儲存所有列表元素的位置,只渲染可視區內的列表元素,當可視區滾動時,根據滾動的offset大小以及所有列表元素的位置,計算在可視區應該渲染哪些元素。
具體實現步驟如下所示:
- 首先確定長列表所在父元素的大小,父元素的大小決定了可視區的寬和高
- 確定長列表每一個列表元素的寬和高,同時初始的條件下計算好長列表每一個元素相對於父元素的位置,並用一個數組來儲存所有列表元素的位置資訊
- 首次渲染時,只展示相對於父元素可視區內的子列表元素,在滾動時,根據父元素的滾動的offset重新計算應該在可視區內的子列表元素。這樣保證了無論如何滾動,真實渲染出的dom節點只有可視區內的列表元素。
- 假設可視區內能展示5個子列表元素,及時長列表總共有1000個元素,但是每時每刻,真實渲染出來的dom節點只有5個。 5.補充說明,這種情況下,父元素一般使用position:relative,子元素的定位一般使用:position:absolute或sticky
通過虛擬列表優化後,同樣的顯示1000個包含圖片的dom,白屏時間會大大的減少。具體效果如下圖所示:
對於比無優化的情況,優化後的虛擬列表渲染速度提升很明顯。
三、通過react-virtualized來優化長列表
社群實現虛擬列表的React元件很多,較為常用的是react-virtualized和react-tiny-virtual-list,前一個較為全面,第二個較為輕量。接下來會分別來介紹這倆個React元件庫。
1、react-virtualized簡介
react-virtualized是一個實現虛擬列表較為優秀的元件庫,react-virtualized提供了一些基礎元件用於實現虛擬列表,虛擬網格,虛擬表格等等,它們都可以減小不必要的dom渲染。此外還提供了幾個高階元件,可以實現動態子元素高度,以及自動填充可視區等等。
react-virtualized的基礎元件包含:
- Grid:用於優化構建任意網狀的結構,傳入一個二維的陣列,渲染出類似棋盤的結構。
- List:List是基於Grid來實現的,但是是一個維的列表,而不是網狀。
- Table:Table也是基於Grid來實現,表格具有固定的頭部,並且可以在垂直方向上滾動
- Masonry:同樣可以在水平方向,也可以在垂直方向滾動,不同於Grid的是可以自定義每個元素的大小,或者子元素的大小也可以是動態變化的
- Collection:類似於瀑布流的形式,同樣可以水平和垂直方向滾動。
值得注意的是這些基礎元件都是繼承於React中的PureComponent,因此當state變化的時候,只會做一個淺比較來確定重新渲染與否 。
除了這幾個基礎元件外,react-virtualized還提供了幾個高階元件,比如ArrowKeyStepper 、AutoSizer、CellMeasurer、InfiniteLoader等,本文具體介紹常用的AutoSizer、CellMeasurer和InfiniteLoader。
- AutoSizer:使用於一個子元素的情況,通過AutoSizer包含的子元素會根據父元素Resize的變化,自動調節該子元素的可視區的寬度和高度,同時調節的還有該子元素可視區真實渲染的dom元素的數目。
- CellMeasurer:這個高階元件可以動態的改變子元素的高度,適用於提前不知道長列表中每一個子元素高度的情況。
- InfiniteLoader:這個高階元件用於Table或者List的無限滾動,適用於滾動時非同步請求等情況
2、react-virtualized基礎元件的使用
下面我們來介紹一下常用的基礎元件Grid、List。
(1)Grid
所有基礎元件基本上都是基於Grid構成的,一個簡單的Grid的例子如下:
import { Grid } from 'react-virtualized';
const list = [
['Jony yu', 'Software Engineer', 'Shenzhen', 'CHINA', 'GUANGZHOU'],
['Jony yu', 'Software Engineer', 'Shenzhen', 'CHINA', 'GUANGZHOU'],
['Jony yu', 'Software Engineer', 'Shenzhen', 'CHINA', 'GUANGZHOU'],
['Jony yu', 'Software Engineer', 'Shenzhen', 'CHINA', 'GUANGZHOU'],
['Jony yu', 'Software Engineer', 'Shenzhen', 'CHINA', 'GUANGZHOU'],
['Jony yu', 'Software Engineer', 'Shenzhen', 'CHINA', 'GUANGZHOU']
];
function cellRenderer ({ columnIndex, key, rowIndex, style }) {
return (
<div
key={key}
style={style}
>
{list[rowIndex][columnIndex]}
</div>
)
}
render(
<Grid
cellRenderer={cellRenderer}
columnCount={list[0].length}
columnWidth={100}
height={300}
rowCount={list.length}
rowHeight={80}
width={300}
/>,
rootEl
);
複製程式碼
顯示的效果如下圖所示:
渲染網格也是隻渲染可視區的dom節點,有個有趣的現象是滾動條的大小,這裡Grid做了一個細節優化,只有滾動的時候才會顯示滾動條,停止滾動後會隱藏滾動條。
(2)List
接著來舉例說明一下List的使用:
import { List } from 'react-virtualized';
import loremIpsum from "lorem-ipsum"
const rowCount = 1000;
const list = Array(rowCount).fill().map(()=>{
return loremIpsum({
count: 1,
units: 'sentences',
sentenceLowerBound: 3,
sentenceUpperBound: 3
}
})
function rowRenderer ({
key,
index,
isScrolling,
isVisible,
style
}) {
return (
<div
key={key}
style={style}
>
{list[index]}
</div>
)
}
export default class TestList extends Component{
render(){
return <div style={{height:"300px",width:"200px"}}>
<List
width={300}
height={300}
rowCount={list.length}
rowHeight={20}
rowRenderer={rowRenderer}
/>
</div>
}
}
複製程式碼
List的使用方法也是極簡,指定列表總條數rowCount,每一條的高度rowHeight以及每次渲染的函式rowRenderer,就可以構建一個渲染列表。具體的效果如下圖所示:
2、react-virtualized高階元件的使用
結合List來看看react-virtualized高階元件的使用。
(1)AutoSizer
首先來看使用不使用AutoSizer的缺點,如下圖所示,List只能指定固定的大小,如果其所在的父元素的大小resize了,那麼List是不會主動填滿父元素的可視區的:
從上圖可以看出來,List是無法自動填充父元素的。因此我們這裡需要使用AutoSizer。AutoSizer的使用也很簡單,我們只需要在List的基礎上:
class TestList extends Component{
render(){
return <div>
<AutoSizer>
{({ height, width }) => (
<List
height={height}
rowCount={list.length}
rowHeight={20}
rowRenderer={rowRenderer}
width={width}
/>
)}
</AutoSizer>
</div>
}
}
複製程式碼
效果如下圖所示:
上述可以看出來增加了AutoSizer可以動態的適應父元素寬度和高度的變化。
但是也存在一個問題:
子元素太長,換行後改變了子元素的高度後無法子適應,也就是說僅僅通過基礎的元件List是不支援子元素的高度動態改變的場景。
(2)CellMeasurer
為了解決上述的子元素可以動態變化的問題,我們可以利用高階元件CellMeasurer:
import { List,AutoSizer,CellMeasurer, CellMeasurerCache} from 'react-virtualized';
const cache = new CellMeasurerCache({ defaultHeight: 30,fixedWidth: true});
function cellRenderer ({ index, key, parent, style }) {
console.log(index)
return (
<CellMeasurer
cache={cache}
columnIndex={0}
key={key}
parent={parent}
rowIndex={index}
>
<div
style={style}
>
{list[index]}
</div>
</CellMeasurer>
);
}
複製程式碼
對於需要渲染的List,如下所示:
class TestList extends Component{
render(){
return <div>
<AutoSizer>
{({ height, width }) => (
<List
height={height}
rowCount={list.length}
rowHeight={cache.rowHeight}
deferredMeasurementCache={cache}
rowRenderer={cellRenderer}
width={width}
/>
)}
</AutoSizer>
</div>
}
}
複製程式碼
最後的結果如下所示:
上圖我們看出來,子列表元素的高度可以動態變化,通過CellMeasurer可以實現子元素的動態高度。
(3)InfiniteLoader
最後我們來考慮這種無限滾動的場景,很多情況下我們可能需要分頁載入,就是常見的在可視區內無限滾動的場景。react-virtualized提供了一個高階元件InfiniteLoader用於實現無限滾動。
InfiniteLoader的使用很簡單,只要按著文件來即可,就是分頁的去在家下一頁,滾動分頁所呼叫的函式為:
function loadMoreRows ({ startIndex, stopIndex }) {
return new Promise(function(resolve,reject){
resolve()
}).then(function(){
//模擬ajax請求
let temList = Array(10).fill(1).map(()=>{
return loremIpsum({
count: 1,
units: 'sentences',
sentenceLowerBound:3,
sentenceUpperBound:3
})
})
list = list.concat(temList)
})
}
複製程式碼
最後的效果如下:
看起來跟基礎元件List一樣,其實唯一的區別就是會在滾動的時候自動執行loadMoreRows函式去更新list
(4)總結
通過基礎元件Grid、List以及高階元件AutoSizer、CellMeasurer和InfiniteLoader,已經可以構建出比較複雜的場景,但是有一個缺陷,就是CellMeasurer雖說一定程度上支援動態子元素的高度的變化,其實是一種估算,存在很多邊界情況,無法適應於動態元素的場景,特別是文字節點較多導致的高度變化。但是對於圖片節點的動態高度支援沒有很大的問題。
舉例一種邊界情況,CellMeasurer無法支援文字動態高度的情況:
從上圖可以看到,慢慢縮小的過程中,如果縮的太小,並沒有動態的撐大子元素的高度,出現了文字的重疊。
四、通過react-tiny-virtual-list來優化長列表
react-tiny-virtual-list是一個較為輕量的實現虛擬列表的元件,不同於react-virtualized支援網格以及表格等渲染優化。 react-tiny-virtual-list只支援列表,使用方便,其原始碼也只有700多行。
使用極其簡單:
import VirtualList from 'react-tiny-virtual-list';
const data = ['A', 'B', 'C', 'D', 'E', 'F','A', 'B', 'C',
'D', 'E', 'F','A', 'B', 'C', 'D', 'E', 'F',
'A', 'B', 'C', 'D', 'E', 'F'];
export default class TinyVirtual extends Component {
render(){
return <VirtualList
width='100%'
height={200}
itemCount={data.length}
itemSize={50} // Also supports variable heights (array or function getter)
renderItem={({index, style}) =>
<div key={index} style={style}>
// The style property contains the item's absolute position Letter: {data[index]}, Row: #{index}
</div>
}
/>
}
}
複製程式碼
最後的渲染結果也是相似的,也可以支援無限滾動等等。
但是react-tiny-virtual-list有一個致命的缺點:
完全不支援子元素的動態高度或者寬度
五、總結
本文介紹了虛擬列表的優化的原理,以及常用的React可以優化虛擬列表的元件庫。在接下來的文章中,會具體的介紹react-tiny-virtual-list和react-virtualized的原始碼,敬請期待。