3 ways to render large lists in Angular

In 2020, front-end frameworks became better, more efficient and faster. But, even with this in mind, rendering large lists without “freezing” the browser can still be a daunting task even for the fastest existing frameworks.

This is one of those cases where "the framework is fast and the code is slow." There are many approaches that allow you to display a large number of elements without blocking user interaction with the web page. The author of the article, the translation of which we are publishing today, wants to explore the existing methods for displaying large lists on web pages and talk about their areas of application.







Although this material is aimed at Angular, what is being discussed here applies to other frameworks and projects that are written in pure JavaScript. In particular, the following approaches to rendering large lists will be considered here:

  • Virtual scrolling (using Angular CDK).
  • Manual rendering.
  • Progressive rendering.

Virtual scrolling


Virtual scrolling is perhaps the most efficient way to work with large lists. True, he has some disadvantages. Virtual scrolling, thanks to the Angular CDK and other plugins, is very easy to implement in any component.

The idea behind virtual scrolling is simple, but it is not always easy to implement. The bottom line here is that we have a container and a list of elements. An element is rendered only if it is within the visible area of ​​the container.

We will use the module scrollingfrom the Angular CDK, which is designed to organize virtual scrolling. To do this, you first need to install the CDK:

npm i @angular/cdk

Then you need to import the module:

import { ScrollingModule } from '@angular/cdk/scrolling';
@NgModule({
 ...
 imports: [ ScrollingModule, ...]
})
export class AppModule {}

After that, in the components you can use cdk-virtual-scroll-viewport:

<cdk-virtual-scroll-viewport itemSize="50">
 <div *cdkVirtualFor="let item of items">
   {{ item }}
 </div>
</cdk-virtual-scroll-viewport>

Here is an example project that uses this approach to organizing virtual scrolling.

As you can see, the standard Angular mechanisms allow you to achieve impressive results without much difficulty. The component in the example renders many thousands of elements without any problems.

If virtual scrolling is so good, and if it is so easy to implement, the question arises as to why study other ways to render large lists. I was also interested in this question. As it turned out, this state of affairs has several reasons:

  • , , . , . , Autocomplete ( ). , , , , . — .
  • — , .
  • There are some problems with accessibility and ease of use of the contents of a list with virtual scrolling. Hidden elements are not rendered - this means that they will not be available for screen readers, and that they cannot be found on the page using standard browser mechanisms.

Virtual scrolling is ideal in a number of situations (provided that it works):

  • In that case, if you want to display lists whose size is not known in advance, or those that can be huge (according to a rough estimate, lists that include more than 5 thousand elements, but this greatly depends on the complexity of each element).
  • In the event that you need to organize endless scrolling.

Manual rendering


One of the ways to work with lists that I tried to speed up the output of a large set of elements is to use manual rendering using the Angular API instead *ngFor.

We have a simple template that uses a loop organized using the * directivengFor

<tr 
*ngFor="let item of data; trackBy: trackById; let isEven = even; let isOdd = odd"
    class="h-12"
    [class.bg-gray-400]="isEven"
    [class.bg-gray-500]="isOdd"
>
  <td>
    <span class="py-2 px-4">{{ item.id }}</span>
  </td>

  <td>
    <span>{{ item.label }}</span>
  </td>

  <td>
    <a>
      <button class="py-2 px-4 rounded (click)="remove(item)">x</button>
    </a>
  </td>
</tr>

To measure the rendering performance of 10,000 simple elements, I used a benchmark based on js-frameworks-benchmark .

First, I examined the performance of a list, which uses a regular loop to output *ngFor. As a result, it turned out that the execution of the code (Scripting) took 1099 ms., The rendering (Rendering) took 1553 ms., And the drawing (Painting) - 3 ms.


A study of the performance of a list, which uses * ngFor to output, List

items can be rendered manually using the Angular API:

<tbody>
  <ng-container #itemsContainer></ng-container>
</tbody>
<ng-template #item let-item="item" let-isEven="isEven">
  <tr class="h-12 "
      [class.bg-gray-400]="isEven"
      [class.bg-gray-500]="!isEven"
  >
    <td>
      <span class="py-2 px-4">{{ item.id }}</span>
    </td>

    <td>
      <span>{{ item.label }}</span>
    </td>

    <td>
      <a>
        <button class="py-2 px-4 rounded" (click)="remove(item)">x</button>
      </a>
    </td>
  </tr>
</ng-template>

Let's talk about how the controller code has changed.

We declared a template and container:

@ViewChild('itemsContainer', { read: ViewContainerRef }) container: ViewContainerRef;
@ViewChild('item', { read: TemplateRef }) template: TemplateRef<any>;

When generating data, we render them using the createEmbeddedViewentity method ViewContainerRef:

private buildData(length: number) {
  const start = this.data.length;
  const end = start + length;

  for (let n = start; n <= end; n++) {
    this.container.createEmbeddedView(this.template, {
      item: {
        id: n,
        label: Math.random()
      },
      isEven: n % 2 === 0
    });
  }
}

As a result, the indicators characterizing the performance of the list were slightly improved. Namely, it took 734 ms to execute the code, 1443 ms to render, 2 ms to draw.


Researching the performance of a list that uses the Angular API to output

True, in practice, this means that the list is still extremely slow. When you click on the appropriate button, the browser “freezes” for a few seconds. If this appeared in production, users would definitely not like it.

Here's what it looks like (here I, using the mouse, imitate the loading indicator).


List Slowing

Now, to improve manual list rendering, we’ll try to apply progressive rendering technology.

Progressive rendering


The idea of ​​progressive rendering is simple. It consists in rendering a subset of elements progressively, postponing the rendering of other elements in the event loop. This allows the browser to display all the elements without "slowing down".

The code below that implements progressive list rendering is very straightforward:

  • First, using this setInterval, we establish a regular, every 10 ms. Call to the function that renders 500 elements when called.
  • After all the elements are deduced, which we determine based on the analysis of the index of the current element, we stop the regular function call and interrupt the cycle.

private buildData(length: number) {
  const ITEMS_RENDERED_AT_ONCE = 500;
  const INTERVAL_IN_MS = 10;

  let currentIndex = 0;

  const interval = setInterval(() => {
    const nextIndex = currentIndex + ITEMS_RENDERED_AT_ONCE;

    for (let n = currentIndex; n <= nextIndex ; n++) {
      if (n >= length) {
        clearInterval(interval);
        break;
      }
      const context = {
        item: {
          id: n,
          label: Math.random()
        },
        isEven: n % 2 === 0
      };
      this.container.createEmbeddedView(this.template, context);
    }

    currentIndex += ITEMS_RENDERED_AT_ONCE;
  }, INTERVAL_IN_MS);

Please note that the number of elements displayed per call to the function being transferred setInterval, as well as the frequency of the call to this function, are completely dependent on the circumstances. For example, if the elements of the displayed list are very complex, then the output of 500 such elements in one go will be unacceptably slow.

By measuring the performance of this solution, I got results that look worse than those that I received earlier. Code execution - 907 ms., Rendering - 2555 ms., Drawing - 16 ms.


Researching the performance of a list, the output of which uses progressive rendering.

But the user, working with such a list, will experience a much more pleasant experience than before. Although the time required to render the list has increased, the user will not notice it. We, in one go, render 500 elements. In this case, rendering is performed outside the boundaries of the container.

Here, some problems may arise due to the fact that the container changes size during rendering, or the position of the content scrolling changes. If this happens, you will have to deal with similar problems.

Here's what working with such a list looks like.


List is fast

Summary


Manual and progressive rendering techniques for large lists are certainly useful in some situations. I used them in cases where virtual scrolling for some reason did not suit me.

Given the above, we can say that most often virtual scrolling, built on the basis of a good library, such as the Angular CDK, is the best way to display large lists. But if virtual scrolling cannot be used for some reason, the developer has other possibilities.

Dear readers! How do you output large lists in your web projects?


All Articles