Talk about long lists in front-end development

Front-end business development will encounter some lists that can not be loaded by paging, which we generally call long lists. In this article, we define long lists as lists with data lengths greater than 1000 and cannot be presented in paginated form.

This article explores the following topics:

Is it possible to optimize the long list of full rendering? Where is the limit of optimization?

If we use incomplete rendering long list, what are the schemes and specific implementation ideas?

The content of this article has nothing to do with the specific framework, but is implemented in some examples using Vue.

Complete rendering long list

How long does it take to render a long list without any optimization? First of all, you need to understand the time consumed to create all HTMLElement s and add them to Document, because there will be some other code mixed up in the business, and your business will not perform faster than this time. A general understanding of the performance of browser-created elements is needed to know where the optimization limit for long lists is.

We can write a simple way to test this performance:

var createElements = function(count) {
  var start = new Date();

  for (var i = 0; i < count; i++) {
    var element = document.createElement('div');
    element.appendChild(document.createTextNode('' + i));
    document.body.appendChild(element);
  }

  setTimeout(function() {
    alert(new Date() - start);
  }, 0);
};

We bind an onclick event to a Button that calls createElements(10000);. The data from Chrome's Profile tab are as follows:

Event Click only executed 20.20 ms, and the rest of the time totaled 450 ms, as follows:

Event Click: 20.20ms

Recalculage Style: 16.86ms

Layout: 410.6ms

Update Layer Tree: 11.93ms

Paint: 9.2ms

Method of Detecting Rendering Time

You may notice that the time calculation process in the test code above does not directly calculate the time after calling the API, but instead uses a setTimeout, which is explained below.

The simplest way to calculate the execution time of a piece of code is to write as follows:

var start = Date.now();

// ...

alert(Date.now() - start);
But it is not scientific to test the performance of DOM because the operation of DOM will cause the reflow of browser. If the reflow execution time of browser is much longer than the code execution time, it will cause the browser to be still in carton after your time calculation is completed. Statistical time should be from "start to create elements" to "be able to respond", so it is reasonable to put the calculation in setTimeout(function() {}, 0). callback in setTimeout() is deferred until the browser's main thread reflow ends, which is basically the same time as Profile in Chrome Devtools, and can be trusted as rendering time.

The modified code is as follows:

var start = Date.now();

// ...
setTimeout(function() {
alert(Date.now() - start);
}, 0);
If you need more precision, you can replace Date.now() with performance.now(), which can be as accurate as one thousandth of a millisecond.

Try using different DOM API s

In the past few years, optimizing element creation performance often referred to the use of createDocument Fragment, innerHTML instead of createElement, through "createElement vs createDocument Fragment" can find a considerable number of test results. This article even says "using Document Fragments to append about 2700 times faster than appending with innerHTML." We can do a simple experiment to see if this conclusion still applies in Google Chrome.

We will test the following four cases separately:

Create an empty element and immediately add it to the document.

Create an element that contains text and add it to the document immediately.

Create a Document Fragment to save list items, and then add the Document Fragment to the document.

Spell out the HTML of all list items and assign values using the innerHTML attribute of the element.

The reasons for the first test are as follows: using empty elements and elements with text nodes, the performance difference is about five times.

The methods for creating empty elements are as follows:

var createEmptyElements = function(count) {
  var start = new Date();

  for (var i = 0; i < count; i++) {
    var element = document.createElement('div');
    document.body.appendChild(element);
  }

  setTimeout(function() {
    alert(new Date() - start);
  }, 0);
};

The methods for creating text elements are as follows:

var createElements = function(count) {
  var start = new Date();

  for (var i = 0; i < count; i++) {
    var element = document.createElement('div');
    element.appendChild(document.createTextNode('' + i));
    document.body.appendChild(element);
  }

  setTimeout(function() {
    alert(new Date() - start);
  }, 0);
};

The method of using Document Fragment is as follows:

var createElementsWithFragment = function(count) {
  var start = new Date();
  var fragment = document.createDocumentFragment();

  for (var i = 0; i < count; i++) {
    var element = document.createElement('div');
    element.appendChild(document.createTextNode('' + i));
    fragment.appendChild(element);
  }
  document.body.appendChild(fragment);

  setTimeout(function() {
    alert(new Date() - start);
  }, 0);
};

The method of using innerHTML is as follows:

var createElementsWithHTML = function(count) {
  var start = new Date();
  var array = [];

  for (var i = 0; i < count; i++) {
    array.push('<div>' + i + '</div>');
  }

  var element = document.createElement('div');
  element.innerHTML = array.join('');
  document.body.appendChild(element);

  setTimeout(function() {
    alert(new Date() - start);
  }, 0);
};

data statistics

There will be some errors in the calculation time of the test code each time it is executed. The data in the table uses the average of 10 tests:

As a result, only innerHTML has a 10% performance advantage, while createElement and createDocument Fragment have almost the same performance. For modern browsers, the performance bottleneck is not in the stage of invoking DOM API at all. Whatever way DOM API is used to add elements, the impact on performance is minimal.

Incomplete Rendering Long List

As can be seen from the test results above, it takes 500 ms + to create 10,000 nodes, and about 20 nodes for each node in the list of actual business. Then, 500ms can only render about 500 list items.

So the long list of complete rendering is basically difficult to meet the business requirements. There are two ways to render the long list of incomplete rendering:

Lazy rendering: This is the usual infinite scrolling, rendering only one part (for example, 10 bars) at a time, and then rendering the other part when the rest of the scrolls to the visible area.

Visible Area Rendering: Render only the visible part, but not the invisible part.

Lazy rendering

Lazy rendering is what we usually call infinite scrolling, referring to scrolling to the bottom of the page, then loading the remaining data. This is a way of front-end and back-end co-optimization. Loading fewer data at a time in the back end can save traffic, and rendering fewer data at the front end for the first time will be faster. This optimization requires that the product side must accept this form of list, otherwise it cannot be optimized in this way.

The idea is very simple: listen for scroll events of the parent element (usually window), determine whether the page is at the bottom of the page by scrollTop of the parent element, and load more data if it is at the bottom of the page.

This article uses Vue to implement a simple example, in which the scrollable area is on the window, with only three lines of core code:

const maxScrollTop = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight) - window.innerHeight;
const currentScrollTop = Math.max(document.documentElement.scrollTop, document.body.scrollTop);
​
if (maxScrollTop - currentScrollTop < 20) {
  //...
}

You can click here to view the online Demo or run it locally through the complete code:

<template>
  <div class="lazy-list">
    <div class="lazy-render-list-item" v-for="item in data">{{ item }}</div>
  </div>
</template><style>
  .lazy-render-list {
    border: 1px solid #666;
  }.lazy-render-list-item {
    padding: 5px;
    color: #666;
    height: 30px;
    line-height: 30px;
    box-sizing: border-box;
  }
</style><script>
  export default {
    name: 'lazy-render-list',
​
    data() {
      const count = 40;
      const data = [];
​
      for (let i = 0; i < count; i++) {
        data.push(i);
      }
​
      return {
        count,
        data
      };
    },
​
    mounted() {
      window.onscroll = () => {
        const maxScrollTop = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight) - window.innerHeight;
        const currentScrollTop = Math.max(document.documentElement.scrollTop, document.body.scrollTop);
​
        if (maxScrollTop - currentScrollTop < 20) {
          const count = this.count;
          for (let i = count; i < count + 40; i++) {
            this.data.push(i);
          }
          this.count = count + 40;
        }
      };
    }
  };
</script>

If it is used in production, it is recommended to use mature class libraries, which can be searched by "framework name + infinite scroll".

Visual Area Rendering

Visible area rendering refers to rendering only the list items of the visible area, and non-visible areas are not rendered at all. The list items are updated dynamically when the scrollbar scrolls. Visual area rendering is suitable for the following scenarios:

The height of each data presentation form needs to be consistent (not necessary, but the minimum height needs to be determined).

In product design, more than 1000 pieces of data need to be loaded at one time.

In product design, scrollbars need to be mounted in a fixed height area (which is also possible on Windows, but requires the entire area to display only this list).

This article uses Vue to implement an example to illustrate how this type of list can be implemented. This example makes the following three settings:

The height of the list is 400 px.

The height of each element in the list is 30px.

Load 10,000 data at a time.

You can click here to view the online Demo or run it locally through the complete code:

<template>
  <div class="list-view" @scroll="handleScroll($event)">
    <div class="list-view-phantom" :style="{ height: data.length * 30 + 'px' }"></div>
    <div v-el:content class="list-view-content">
      <div class="list-view-item" v-for="item in visibleData">{{ item.value }}</div>
    </div>
  </div>
</template><style>
  .list-view {
    height: 400px;
    overflow: auto;
    position: relative;
    border: 1px solid #666;
  }.list-view-phantom {
    position: absolute;
    left: 0;
    top: 0;
    right: 0;
    z-index: -1;
  }.list-view-content {
    left: 0;
    right: 0;
    top: 0;
    position: absolute;
  }.list-view-item {
    padding: 5px;
    color: #666;
    height: 30px;
    line-height: 30px;
    box-sizing: border-box;
  }
</style><script>
  export default {
    props: {
      data: {
        type: Array
      },
​
      itemHeight: {
        type: Number,
        default: 30
      }
    },
​
    ready() {
      this.visibleCount = Math.ceil(this.$el.clientHeight / this.itemHeight);
      this.start = 0;
      this.end = this.start + this.visibleCount;
      this.visibleData = this.data.slice(this.start, this.end);
    },
​
    data() {
      return {
        start: 0,
        end: null,
        visibleCount: null,
        visibleData: [],
        scrollTop: 0
      };
    },
​
    methods: {
      handleScroll(event) {
        const scrollTop = this.$el.scrollTop;
        const fixedScrollTop = scrollTop - scrollTop % 30;
        this.$els.content.style.webkitTransform = `translate3d(0, ${fixedScrollTop}px, 0)`;
​
        this.start = Math.floor(scrollTop / 30);
        this.end = this.start + this.visibleCount;
        this.visibleData = this.data.slice(this.start, this.end);
      }
    }
  };
</script>

The implementation details in the example code are as follows. You can refer to this illustration to help you understand this example.

Use a phantom element to prop up the entire list and let the scrollbar of the list appear.

The variable visibleData(Array type) is used in the list to record all the data currently needed to be displayed.

The variable visibleCount is used in the list to record the maximum number of data displayed in the visible area.

In the list, start and end variables are used to record the beginning and end indexes of visible region data.

When scrolling, modify the transform of the real display area: translate2d (0, y, 0).

The above is just a simple example. If you want to use it in production, you can recommend Clusterize or React Virtualized.

You may find that infinite scrolling is common on the mobile side, but visible area rendering is not common, mainly because onscroll events of UIWebView on iOS cannot be triggered in real time. I have tried to use iScroll to achieve similar visual area rendering, although the problem of slow initial rendering can be solved, but there will be a problem of poor experience when scrolling (there will be white screen time).

summary

This paper validates the performance bottleneck of long lists through some test data, and illustrates the implementation ideas of two kinds of incomplete rendering through examples, hoping to inspire you.

Keywords: Fragment Vue REST Google

Added by bobthebuilder on Sun, 14 Jul 2019 23:53:40 +0300