Long list performance problems

In the mobile terminal H5 project, the problem we often encounter is the long list display. When the data is small, it is ok. If the data volume is too large, there will be obvious performance problems, especially slow loading and even jammed.

So long list performance optimization is a very common problem. Our project component library uses ANTD-Mobile, so we first want to use ANTD-Mobile ListView, but ANTD Mobile’s solution is to load the data in the visual area first, and then load the next part of the data when sliding down. Elements from previous renderings are not cleared. This can result in a large number of DOM elements on the page, which can also affect performance.

Since we had a fixed row height and a simple scene, we decided to write a dynamically loaded list ourselves.

Ii. Design ideas

Although the list is long, most areas are not visible, so only the elements of the visible area need to be rendered, and the user can update the rendered data as they slide.

Iii. Code design

1. Simplest implementation

The most important thing to realize the scheme is to determine two parameters,

  1. The index of the start data, declared as startIndex
  2. End the index of the data and declare it endIndex

If you look at the figure above, you can see that startIndex is the scrollTop of the background real list divided by the height of each row, and endIndex is the number of items that startIndex plus the viewable area can display. This number is the height of the viewable area divided by the height of each row, which is the following code

Each item line is rowHeight, this is fixed, given a 44px (the REM we use in our project requires only a simple calculation), write code to display the page first.

//./index.jsx import React from 'react'; import './index.less'; let rowHeight = 44; class TestList extends React.Component { constructor(props) { super(props); let data = new Array(100000); for (let index = 0; index < data.length; index++) { data[index] = index; } this.allData = data; this.state = { dataSource: [] }; this.startIndex = 0; } componentDidMount() { const containerH = document.documentElement.clientHeight; this.size = Math.ceil(containerH / rowHeight) + 1; this.endIndex = this.startIndex + this.size; this.setState({ containerH, dataSource: Slice (this.startIndex, this.endIndex)})} Scroll (e) {this.allData.slice(this.startIndex, this.endIndex)})} scroll(e) { } render() { const { dataSource, containerH } = this.state; const renderRow = (rowData, index) => { const top = this.startIndex * rowHeight + rowHeight * index; return ( <div key={rowData} className='item-row' style={{ position: 'absolute', top: top }}> <span>{rowData}</span> </div> ); }; return ( <div style={{ height: containerH }} className='outer-cont' onScroll={(e) => this.scroll(e)} > <div className="inner-cont" style={{ height: this.allData.length * rowHeight }}> {dataSource.map((row, index) => renderRow(row, index))} </div> </div> ); } } export default TestList;Copy the code
//./index.less .outer-cont { position: relative; overflow: auto; .inner-cont { position: relative; .item-row { width: 100%; height: 44px; border-bottom: 1px solid; }}}Copy the code

This implementation only shows the first few pieces of data, and the data cannot be refreshed when sliding. Now add onScroll method based on previous analysis.

Scroll (e) {if (this.alldata.length <= this.size) return; if (this.alldata.length <= this.size) return; const top = e.target.scrollTop; // Get scrollTop, calculate startIndex and endIndex const topIndex = math.floor (top/rowHeight); // If the slip is too small, index does not change. if(this.startIndex === topIndex) return this.startIndex = topIndex; this.endIndex = this.startIndex + this.size; // Console. log(this.startindex); This.setstate ({dataSource: this.allData.slice(this.startIndex, this.endIndex)})}Copy the code

Swiping up and down is fine, but every swipe triggers a data refresh, which is too frequent, so think about making a buffer to buffer more DOM at a time.

Buffer optimization

The basic idea is to render more DOM as a buffer each time you refresh the data, and then update the data when you are about to slide out of the buffer.

The details that need to be worked out now are when to refresh the data.

  1. In the beginning, the viewable area is still within the safe zone, and you can move around it without refreshing the data.
  2. StartIndex needs to be moved down when the user slides and the viewable area is out of the safe zone.
  1. StartIndex, by contrast, needs to move up.
  2. You don’t need to make a distinction between 2 and 3, you just need to move the startIndex so that the topIndex is in the safe zone, and put it in the middle of the buffer zone, and then the algorithm is simple, topIndex = startIndex + cacheSize/2.

Okay, cacheSzie = 20 for now

import React from "react"; import "./index.less"; let rowHeight = 44; Class TestList extends React.Component {constructor(props) {//... this.cacheSize = 20; } componentDidMount() {this.setState({containerH, dataSource: this.allData.slice( this.startIndex, this.endIndex + this.cacheSize ), }); } scroll(e) { if (this.allData.length <= this.size) return; const top = e.target.scrollTop; let topIndex = Math.floor(top / rowHeight); / / determine whether the safety area, the data can be adjusted if (topIndex - this. StartIndex < 3 | | topIndex - this. StartIndex > 17) {/ / in accordance with the above analysis, Reset startIndex let index = topIndex - cacheSize / 2 < 0? 0 : topIndex - this.cacheSize / 2; if (this.startIndex === index) return; this.startIndex = index; console.log(this.startIndex); this.endIndex = this.startIndex + this.size; this.setState({ dataSource: this.allData.slice( this.startIndex, this.endIndex + this.cacheSize ), }); }} render() {// same code as before... ; } } export default TestList;Copy the code

Let’s test it out

The refresh frequency is significantly reduced, with one refresh every 8 lines.

3. The onScroll optimization

There is also the problem of onScroll optimization. When sliding, onScroll triggers a lot of times, which leads to the need to do a lot of calculations. The first thing that comes to mind when optimizing onScroll is anti-shake. The user experience is bad. Add the event.persist() method if you want to use an event in an asynchronous method.

So abandon the method of anti – shake function and post it here for a record.

debounce(fn, delay = 100) {
    let timer = null;

    return function (event) {
      let context = this;
      event.persist && event.persist();
      if (timer) clearTimeout(timer);
      timer = setTimeout(function () {
        fn.call(context, event);
      }, delay);
    };
  }
Copy the code

Although anti-shaking is not good, there are other methods. After checking the data, I found that IntersectionObserver can be used. I haven’t tested this yet and I don’t know how effective it is.