takeaway
This article is intended for three types of readers
- To learn more about virtual lists, read the section before “Implementing a Simple virtual list.
- Want to explore the concrete implementation of virtual list can focus on reading “to achieve a simple virtual list” in the scheme 1
- If you want to further study and discuss how to solve the problem that the height of the list item is not fixed in virtual list, you can read the plan 2 and Plan 3 in “Implementing a Simple virtual list”
preface
At work, we often come across list items. If the number of list items is large, in many cases we will use pagination to load, to avoid loading a large amount of data at once, resulting in page performance problems.
However, after the user browses a large amount of data in the paging load, the list items will gradually increase, and the page may be jammed. Or maybe we need to load a large amount of data at once, presenting all the data to the user at once, rather than page-loading, where the number of items in the list can become so large that the page becomes jammed.
This time we will introduce a virtual list optimization method to solve the problem of list performance when the data volume is large.
What is a virtual list
A virtual list is a technique for displaying on demand, rendering only a portion of the list elements within the viewable area, rather than all list items, based on the user’s scrolling.
As shown in the figure, when there are thousands of list items in the list, we can use virtual list to optimize. You only need to render the 8 list items item8 through Item15 in the viewPort. Since only eight list elements are rendered in the list at all times, this ensures the performance of the list.
Virtual list component
Optimization of long lists is a very complex problem that has always been difficult. This is an example of Antd Design’s List component which is recommended to be used together with the React – Virtualized component to optimize long lists.
The best way to optimize a long list is to use a few ready-made virtualized list components, the most common ones are Act-Virtualized and Act-Tiny-Virtual-List. Then you can effectively optimize your long list by using them.
react-tiny-virtual-list
React-tiny-virtual-list is a lightweight component that implements virtual lists and is easy to use, with only 700 lines of source code. Here’s an example from its website.
import React from 'react'; import {render} from 'react-dom'; import VirtualList from 'react-tiny-virtual-list'; const data = ['A', 'B', 'C', 'D', 'E', 'F', ...] ; render( <VirtualList width='100%' height={600} 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> } />, document.getElementById('root') );Copy the code
react-virtualized
In the React ecosystem, Act-Virtualized has been a long list optimization for a long time, and the community is constantly updating and discussing it, which means it’s a very persistent problem. Compared to the lightweight Games of React Tiny-Virtual-List, React – Virtualized was more comprehensive.
React – Virtualized provides a few basic components for implementing virtual lists, virtual grids, virtual tables, etc. They all minimize unnecessary DOM rendering. Several higher-order components are also provided that allow for dynamic child height, automatic filling of visual areas, and more.
When using Ant Design’s List, we also recommend using React – Virtualized lists to optimize big data.
Implement a simple virtual list
We’ve already seen how virtual lists work: only a portion of the list elements in the viewable area are rendered. Let’s use the idea of a virtual list to implement a simple list component. Here, we present two solutions, both of which combine the paginated pull-down loading mode.
Plan a
The DOM structure of the first option is shown in figure 1
-
Outer container: set height, overflow: scroll
-
Slide list: Absolutely locate, then calculate the slide list height by the list element height * the number of list elements
-
Viewable area: Dynamically calculates the offset of the viewable area in the slider list. Use the Translate3D property to dynamically set the offset of the viewable area, resulting in a sliding effect.
By doing so, only a few DOM elements in the viewable area are rendered at a time, which is really optimized for long lists in the case of big data
However, this is only the case of a list element with fixed height. How to optimize a list with variable height
import React from 'react'; // Props: renderItem: Function<Promise>, getData:Function; height:string; Class InfiniteTwo extends React.Component {constructor(props) {super(props); this.renderItem = props.renderItem this.getData = props.getData this.state = { loading: false, page: 1, showMsg: false, List: [], itemHeight: this.props.itemHeight || 0, start: 0, end: 0, visibleCount: 0 } } onScroll() { let { offsetHeight, scrollHeight, scrollTop } = this.refs.scrollWrapper; let showOffset = scrollTop - (scrollTop % this.state.itemHeight) const target = this.refs.scrollContent target.style.WebkitTransform = `translate3d(0, ${showOffset}px, 0)` this.setState({ start: Math.floor(scrollTop / this.state.itemHeight), end: Math.floor(scrollTop / this.state.itemHeight + this.state.visibleCount + 1) }) if(offsetHeight + scrollTop + 15 > scrollHeight){ if(! this.state.showMsg){ let page = this.state.page; page++; this.setState({ loading: true }) this.getData(page).then(data => { this.setState({ loading: false, page: page, List: data.concat(this.state.List), showMsg: data && data.length > 0 ? false : true }) }) } } } componentDidMount() { this.getData(this.state.page).then(data => { this.setState({ List: RequestAnimationFrame (() => {let {offsetHeight} = this.refs.scrollWrapper; let visibleCount = Math.ceil(offsetHeight / this.state.itemHeight) let end = visibleCount + 1 console.log(this.refs.scrollContent.firstChild.clientHeight) this.setState({ end, visibleCount }) }) }) } render() { const {List, start, end, itemHeight} = this.state const renderList = List.map((item,index)=>{ if(index >=start && index <= end) return( this.renderItem(item, index) ) }) console.log(renderList) return( <div> <div ref="scrollWrapper" onScroll={this.onScroll.bind(this)} style={{height: this.props.height, overflow: 'scroll', position: 'relative'}} > <div style={{height: `${renderList.length * itemHeight}px`, position: 'absolute', top: 0, right: 0, left: 0}}> </div> <div ref="scrollContent" style={{position: 'relative', top: 0, right: 0, left: 0}} > {renderList} < / div > < / div > {this. State. Loading && (< div > < / div >) in the load} {this. State. ShowMsg && (< div > no more content < / div >)} </div> ) } } export default InfiniteTwo;Copy the code
In scenario 1, we set several variables
- Start Specifies the index of the first element rendered
- End Is the index of the last element to render
- VisibleCount Number of visible elements Start + visibleCount = end
- List Indicates the data of all List items
- ShowOffset Specifies the offset of the visual list of elements to be scrollTop – (scrollTop % this.state.itemHeight)
Scheme 2
The DOM structure of the second option is shown in the figure below
-
Outer container: set height, overflow: scroll
-
Top: The height of the element before the viewable area
-
Tail: The height of the element behind the viewable area
-
Viewable area: List elements within a viewable area
When the height is not fixed, we need to dynamically get the height of the element. A good idea is to record the height and position of the DOM after each pull-down load and dom rendering
Because each list element has a different height, calculating the offset can be complicated. Since the height and position of each element are recorded during each pull-down load, why not record height and position information on a page basis
import React from 'react'; // Props: renderItem: Function<Promise>, getData:Function; height:string; Class InfiniteOne extends React.Com {constructor(props) {super(props); // Class InfiniteOne extends React.Com {constructor(props) {super(props); this.renderItem = props.renderItem this.getData = props.getData this.state = { loading: false, page: 0, showMsg: false, List: [] } this.pageHeight = [] } onScroll() { let { offsetHeight, scrollHeight, scrollTop } = this.refs.scrollWrapper; // Determine the list to display, Let ListShow = [...this.state.List] listshow.foreach ((item, index) => { if(this.pageHeight[index]){ let bottom = this.pageHeight[index].top + this.pageHeight[index].height if((bottom < scrollTop - 50) || (this.pageHeight[index].top > scrollTop + offsetHeight + 50)){ ListShow[index].visible = false }else{ ListShow[index].visible = true } } }) this.setState({ List: ListShow }) if(offsetHeight + scrollTop + 5 > scrollHeight){ if(! this.state.showMsg){ let page = this.state.page; page++; this.setState({ loading: true }) this.getData(page).then(data => { this.setState(prevState => { let List = [...prevState.List] List[page] = {data, visible: true} return { loading: false, page: page, List: List, showMsg: data && data.length > 0 ? false : True}}) // after updating the DOM, RequestAnimationFrame (() => {const target = this.refs[' page${page} '] let top = 0; if(page > 0){ top = this.pageHeight[page - 1].top + this.pageHeight[page - 1].height } this.pageHeight[page] = {top, height: target.offsetHeight} }) }) } } } componentDidMount() { this.getData(this.state.page).then(data => { this.setState((prevState) => { let List = [...prevState.List] List[this.state.page] = {data, visible: true} return {List} }) requestAnimationFrame(() => { this.pageHeight[0] = {top: 0, height: this.refs['page0'].offsetHeight} }) }) } render() { const {List} = this.state let headerHeight = 0; let bottomHeight = 0; let i = 0; for(; i < List.length; i++){ if(! List[i].visible){ headerHeight += this.pageHeight[i].height }else{ break; } } for(; i < List.length; i++){ if(! List[i].visible){ bottomHeight += this.pageHeight[i].height } } const renderList = List.map((item,index)=>{ if(item.visible){ return <div ref={`page${index}`} key={`page${index}`}> {item.data.map((value, log) => { return( this.renderItem(value, `${index}-${log}`) ) })} </div> } }) console.log(renderList) return( <div ref="scrollWrapper" onScroll={this.onScroll.bind(this)} style={{height: this.props.height, overflow: 'scroll'}} > <div style={{height: headerHeight}}></div> {renderList} <div style={{height: BottomHeight}} > < / div > {this. State. Loading && (< div > < / div >) in the load} {this. State. ShowMsg && (< div > no more content < / div >)} < / div >) } } export default InfiniteOne;Copy the code
In scheme two, we set several variables
- List: Data for all List items. List is an array, the data attribute of each item stores a page of data, the visible attribute is used to judge whether to render the page data when render, and the visible attribute of each item in the List will be dynamically updated when scrolling, so as to control the elements to be rendered.
- PageHeight: Indicates the position of all items. PageHeight is also an array. The top attribute of each item indicates how far the top of the page scrolls, and height indicates the height of the page. PageHeight is used to update the visible property of each item in the List array according to scrollTop while scrolling.
Scheme comparison
Scheme 2 implements components that support highly inconsistent list elements compared to Scheme 1. Is plan 2 basically enough to meet the needs?
Apparently not. As we said in the introduction and above, virtual lists are used for long list optimization (loading thousands of items at once). The height and position of the list in scheme 2 are calculated after each pull-down load is completed. The list height and position also determine the height of headerHeight and bottomHeight (i.e., the height of the two unrendered areas in the list). So the idea of plan two doesn’t apply directly to long lists. We wanted to study The Act-Tiny-Virtual-list and Act-Virtualized first to get some thoughts on improvement.
Component analysis
First of all, I read the source code of the react-tiny-virtual-list with the help of the article react-tiny-virtual-list. Although react-tiny-virtual-list can be pulled down and scrolled indefinitely, the dynamic height of the list elements, Not supported. The height of each element needs to be explicitly specified.
Then we virtualized on the Act-Virtualized component, which is better than the Act-tiny-virtual-list, but we still need to specify the heights of each element.
Then we learned that there might be other methods to solve the problem of infinite rolling when the elements are not virtualized.
The react-virtualized team realized this and provided a CellMeasurer module that dynamically calculates the size of the child elements. Well, the element has already been loaded at the time of the computation, so what’s the use of the computation? The method used here is: The estimated column width or row height is calculated before the cell element is rendered, which may not be accurate. After the cell element is rendered, the true size can be obtained, so the true size can be cached. The next re-render of the component corrects the calculation of the original estimate to a more accurate value.
We can also take this idea and adapt plan two a little bit to deal with long lists. For convenience, we write a separate component to deal with long lists; For pull-down loading, plan 2 is still used.
-
Outer container: set height, overflow: scroll
-
Top: The height of the element before the viewable area
-
Tail: the estimated height is first used for calculation, and then the actual height is obtained for adjustment in the process of scrolling down
-
Viewable area: List elements within a viewable area
Plan 3
In this case, we need to optimize plan 2. First we need an estimated list height in the attributes our component receives. Then you need to receive a list of data, resource. Then, we divided the data into pages according to the idea of plan 2. We start by calculating head height and bottomHeight with the estimated height to split the rolling container. Dynamically updates the height of the page number stored as you slide to the page you want to load.
import React from 'react'; // Props: renderItem: Function<Promise>, height:string; EstimateHeight: Number, the resource: Class InfiniteThree extends React.Com {constructor(props) {super(props); this.renderItem = props.renderItem this.getData = props.getData this.estimateHeight = Number(props.estimateHeight) * 10 // A page of 10 data, This. Resource = props. Resource this. ListLength = props. Resource Let array = [] for(let I = 0; i < props.resource.length; i++){ if(i % 10 === 0 && i || i === (props.resource.length - 1)){ pageList.push({ data: array, visible: False}) array = []} array.push(props. Resource [I])} pageList[0].visible = true This. pageHeight = [] for(let I = 0; i < this.listLength; i++){ if(i === 0){ this.pageHeight.push({ top: 0, height: this.estimateHeight, isComputed: false, }) }else{ this.pageHeight.push({ top: this.pageHeight[i-1].top + this.pageHeight[i-1].height, height: this.estimateHeight, isComputed: false }) } this.state = { loading: false, page: 0, showMsg: false, List: pageList, } } } onScroll() { requestAnimationFrame(() => { let { offsetHeight, scrollHeight, scrollTop } = this.refs.scrollWrapper; // Determine the list to display, Let ListShow = [...this.state.List] listshow.foreach ((item, index) => { if(this.pageHeight[index]){ let bottom = this.pageHeight[index].top + this.pageHeight[index].height if((bottom < scrollTop - 5) || (this.pageHeight[index].top > scrollTop + offsetHeight + 5)){ ListShow[index].visible = False}else{// Make it visible when it is in view according to the estimated height. This.setstate (prevState => {let List = [... prevstate.list] List[index]. Visible = true return {List}}) // Let target = this.refs[' page${index} '] let top = 0; if(index > 0){ top = this.pageHeight[index - 1].top + this.pageHeight[index - 1].height } if(target && target.offsetHeight && ! ListShow[index].isComputed){ this.pageHeight[index] = {top, height: target.offsetHeight} console.log(target.offsetHeight) ListShow[index].visible = true ListShow[index].isComputed = true This. setState({List: ListShow,})}else{this.pageHeight[index] = {top, height: this.estimateHeight} } } } }) }) } componentDidMount() { } render() { let {List} = this.state let headerHeight = 0; let bottomHeight = 0; let i = 0; for(; i < List.length; i++){ if(! List[i].visible){ headerHeight += this.pageHeight[i].height }else{ break; } } for(; i < List.length; i++){ if(! List[i].visible){ bottomHeight += this.pageHeight[i].height } } const renderList = List.map((item,index)=>{ if(item.visible){ return <div ref={`page${index}`} key={`page${index}`}> {item.data.map((value, log) => { return( this.renderItem(value, `${index}-${log}`) ) })} </div> } }) return( <div ref="scrollWrapper" onScroll={this.onScroll.bind(this)} style={{height: 400, overflow: 'scroll'}} > <div style={{height: headerHeight}}></div> {renderList} <div style={{height: BottomHeight}} > < / div > {this. State. Loading && (< div > < / div >) in the load} {this. State. ShowMsg && (< div > no more content < / div >)} < / div >) } } export default InfiniteThree;Copy the code
In scheme 3, we add isComputed property to each item of the pageHeight array based on scheme 2, and the height of each item is the value of estimateHeigh (estimated height) used during initialization. IsComputed is set to true only after the height of this item is updated with the real height.
It is important to note that the estimated height should be greater than or equal to the actual height so that the container can be spread apart.
summary
This article first introduces an optimization method called “virtual list”, which can optimize lists. We then introduce two mainstream virtual list components that can be used to optimize lists in daily development. Then two kinds of virtual list implementation methods are given and compared. Finally, we studied the characteristics and thoughts of the act-tiny-virtual-list and Act-Virtualized components, and then improved on the basis of Scenario two, and gave a virtualized list optimization scheme for long lists (lists that display a large amount of data once).
Code Demo address
Virtual list practice demo
Refer to the article
- Ele. me front end: Talk about the realization of the front end virtual list
- Nuggets: How to optimise long lists gracefully in React
- Github: React-tiny-virtual-list
- Github: Implementation of Act-Virtualized Components virtual List
- Github: Virtual List Optimization analysis of React – Virtualized Components