At work, we sometimes run into business situations where we need to load list data without pagination, and for this we call such lists long lists. For example, in some foreign exchange trading systems, the front end will display the user’s position status (profit, loss, number of hands, etc.) in real time. At this time, the user’s position list generally cannot be pagination.

In the high Performance Rendering of 100,000 pieces of data (time sharding) article, we mentioned that we can use time sharding to render long lists, but this method is more suitable for the DOM structure of list items is very simple. This article shows you how to use virtual lists to load large amounts of data simultaneously.

Let’s say our long list needs to display 10,000 records, and we render 10,000 records to the page at the same time. Let’s see how long it takes:

<button id="button">button</button><br>
<ul id="container"></ul>  
Copy the code
document.getElementById('button').addEventListener('click'.function(){
    // Record the start time of the task
    let now = Date.now();
    // Insert 10,000 bytes of data
    const total = 10000;
    // Get the container
    let ul = document.getElementById('container');
    // Insert the data into the container
    for (let i = 0; i < total; i++) {
        let li = document.createElement('li');
        li.innerText = ~~(Math.random() * total)
        ul.appendChild(li);
    }
    console.log('JS runtime: '.Date.now() - now);
    setTimeout(() = >{
      console.log('Total elapsed time:'.Date.now() - now);
    },0)
 
    // print JS runtime: 38
    // print total runtime: 957
  })
Copy the code

When we click the button, 10,000 records will be added to the page at the same time. According to the output of the console, we can roughly calculate that the running time of JS is 38ms, but the total time after rendering is 957ms.

A brief explanation of why the two console.log results are so different, and how simple it is to count JS runtime and total render time:

In the JS Event Loop, the rendering thread is triggered to render the page after all the events in the execution stack managed by the JS engine and all the microtask events have been executed. The first console.log is triggered before the page renders. The second console.log is placed in setTimeout, and its trigger time is when the rendering is complete. For details about Event Loop execution, please refer to this article –>

Then, we use Chrome’s Performance tool to analyze the Performance bottleneck of this code in detail:

As can be seen from Performance, the code consumed 960.8ms from execution to rendering, with the main time consumption as follows:

Event(click) : 40.84ms Recalculate Style: 105.08 MS Layout: 731.56 MS Update Layer Tree: 58.87 MS Paint: From this we can see that the two most time-consuming stages of our code execution are Recalculate Style and Layout.

Recalculate Style: Style calculation, the browser calculates which elements should apply which rules according to the CSS selector, and determines the specific Style of each element. Once you know which rules apply to an element, the browser calculates how much space it should take up and where it should fit on the screen. In real life, list items must not consist of a single LI tag, as in the example, but must consist of complex DOM nodes.

As you can imagine, when the list items are too many and the structure of the list items is complex, the rendering will consume a lot of time in the Recalculate Style and Layout stages.

And virtual list is a kind of realization to solve this problem.

What is a virtual list Virtual list is actually an implementation of on-demand display, that is, only the visible area is rendered, not rendering or partial rendering of the data in the non-visible area, so as to achieve extremely high rendering performance.

Assuming 10,000 records need to be rendered simultaneously, our screen visibility area is 500px high and list items are 50px high, then we can only see 10 list items on screen at most, so we only need to load 10 items on the first rendering.

After the first load, we can analyze the list items that should be displayed in the visible area of the screen by calculating the current scroll value when scrolling occurs.

If the scroll occurs and the scroll bar is 150px from the top, we know that the list items in the visible area are items 4 through 13.

implementation

The realization of virtual list is actually to load only the list items needed in the visual area when the first screen is loaded. When scrolling occurs, the list items in the visual area are dynamically obtained through calculation, and the list items in the non-visual area are deleted.

Calculate the startIndex of the current viewable region calculate the endIndex of the current viewable region calculate the data of the current viewable region and render it to the page calculate the startIndex of the data offset in the entire list startOffset and set it to the list

Since only list items within the viewable area are rendered, the Html structure is designed to maintain the height of the list container and trigger the normal scrolling:

<div class="infinite-list-container">
    <div class="infinite-list-phantom"></div>
    <div class="infinite-list">
      <! -- item-1 -->
      <! -- item-2 -->
      <! -... -->
      <! -- item-n -->
    </div>
</div>
Copy the code

Infinite list-container is the container for the visual area, and the height is the total list height, used to form the scroll bar. Infinite list is the render area for the list items. Listen for the Scroll event of infinite-list-Container to obtain the scroll position scrollTop

If we assume that the viewable area is fixed in height, we can call it screenHeight. If we assume that each item is fixed in height, we can call it itemSize. If we assume that the current scrolling position is called scrollTop, we can calculate:

ListHeight = ListData. length * itemSize Number of list items that can be displayed visibleCount = Math.ceil(screenHeight/itemSize) startIndex of data Math.floor(scrollTop/itemSize) endIndex = startIndex + visibleCount List displays data as visibleData = Slice (startIndex,endIndex) After scrolling, I need to get an offset startOffset to offset the render area to the viewable area with style control because the render area is already offset from the viewable area.

Offset startOffset = scrollTop – (scrollTop % itemSize); The resulting simple code looks like this:

VirtualList. Vue components

<template>
  <div ref="list" class="infinite-list-container" @scroll="scrollEvent($event)">
    <div class="infinite-list-phantom" :style="{ height: listHeight + 'px' }"></div>
    <div class="infinite-list" :style="{ transform: getTransform }">
      <div
        ref="items"
        class="infinite-list-item"
        v-for="item in visibleData"
        :key="item.id"
        :style="{ height: itemSize + 'px', lineHeight: itemSize + 'px' }"
      >
        {{ item.value }}
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'VirtualList'.props: {
    // All list data
    listData: {
      type: Array.default: () = >[]},// Each height
    itemSize: {
      type: Number.default: 200}},computed: {
    // List total height
    listHeight() {
      return this.listData.length * this.itemSize
    },
    // Number of list items that can be displayed
    visibleCount() {
      return Math.ceil(this.screenHeight / this.itemSize)
    },
    // The offset corresponds to the style
    getTransform() {
      return `translate3d(0,The ${this.startOffset}px,0)`
    },
    // Get the actual display list data
    visibleData() {
      return this.listData.slice(this.start, Math.min(this.end, this.listData.length))
    }
  },
  mounted() {
    this.screenHeight = this.$el.clientHeight
    this.start = 0
    this.end = this.start + this.visibleCount
  },
  data() {
    return {
      // View area height
      screenHeight: 0./ / the offset
      startOffset: 0.// Start index
      start: 0.// End the index
      end: null}},methods: {
    scrollEvent() {
      // The current scroll position
      const scrollTop = this.$refs.list.scrollTop
      // Start index at this point
      this.start = Math.floor(scrollTop / this.itemSize)
      // End index at this point
      this.end = this.start + this.visibleCount
      // The offset at this time
      this.startOffset = scrollTop - (scrollTop % this.itemSize)
    }
  }
}
</script>

<style scoped>
.infinite-list-container {
  height: 100%;
  overflow: auto;
  position: relative;
  -webkit-overflow-scrolling: touch;
}

.infinite-list-phantom {
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  z-index: -1;
}

.infinite-list {
  left: 0;
  right: 0;
  top: 0;
  position: absolute;
  text-align: center;
}

.infinite-list-item {
  padding: 10px;
  color: # 555;
  box-sizing: border-box;
  border-bottom: 1px solid # 999;
}
</style>
Copy the code

Component debugging code

<template>
  <div id="app">
    <VirtualList :listData="data" :itemSize="100" />
  </div>
</template>

<script>
import VirtualList from '@/components/VirtualList'

const d = []
for (let i = 0; i < 1000; i += 1) {
  d.push({ id: i, value: i })
}

export default {
  name: 'App'.data() {
    return {
      data: d
    }
  },
  components: {
    VirtualList
  }
}
</script>

<style>
html {
  height: 100%;
}
body {
  height: 100%;
  margin: 0;
}
#app {
  height: 100%;
}
</style>
Copy the code