loading...
Cover image for 3 Ways to Render Large Lists in Angular
Angular

3 Ways to Render Large Lists in Angular

gc_psk profile image Giancarlo Buomprisco Originally published at blog.bitsrc.io ・6 min read

An overview of the available techniques to render large lists of items with Angular

This article was originally published on Bits and Pieces by Giancarlo Buomprisco

Frameworks in 2020 got better, more efficient and faster. With that said, rendering large lists of items on the Web without causing the Browser to freeze can still be hard even for the fastest frameworks available.

This is one of the many cases where “the framework is fast, your code is slow”.

There are many different techniques that make rendering a large number of items in a non-blocking way for the users. In this article, I want to explore the current techniques available, and which ones are best to use based on particular use-cases.

Although this article focuses on how to optimize rendering with Angular, these techniques are actually applicable to other frameworks or simply Vanilla Javascript.

The framework is fast, your code is slow

This article goes in detail about an aspect I talked about in one of my previous articles: rendering too much data.
Top Reasons Why Your Angular App Is Slow

We will take a look at the following techniques:

  • Virtual Scrolling (using the Angular CDK)

  • Manual Rendering

  • Progressive Rendering


Whatever implementation you choose for rendering long lists, make sure you share your reusable Angular components to Bit.dev’s component hub. It will save you time otherwise spent on repeating yourself and will make it easier for you and your team to use tested and performance-optimized code across your Angular projects.

You can read more about it in my previous post:
Sharing Components with Angular and Bit
*An Introduction to Bit: Building and sharing Angular components*blog.bitsrc.io


1. Virtual Scrolling

Virtual Scrolling is probably the most efficient way of handling large lists, with a catch. Thanks to the Angular CDK and other plugins it is very easy to implement in any component.

The concept is simple, but the implementation is not always the easiest:

  • given a container and a list of items, an item is only rendered if it’s within the visible boundaries of the container

To use the CDK’s Scrolling module, we first need to install the module:

npm i @angular/cdk

Then, we import the module:

    import { ScrollingModule } from '@angular/cdk/scrolling';

    @NgModule({
     ...
     imports: [ ScrollingModule, ...]
    })
    export class AppModule {}  

We can now use the components to use virtual scrolling in our components:

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

As you can see, this is extremely easy to use and the results are impressive. The component renders thousands and thousands of items without any problem.

If Virtual Scrolling is so good and easy to achieve, why bother exploring other techniques? This is something I’ve been wondering too — and actually there’s more than one reason as to why.

  • The way it’s going to work is very dependent on implementation: it’s hard to be able to manage all the possible scenarios with one single implementation.
    For example, my component depended on the Autocomplete field (built by the same team) and unfortunately, it didn’t work as expected. The more complex your items, the more difficult it’s going to be.

  • Another module, another big chunk of code added to your app.

  • Accessibility and Usability: the hidden items are not rendered, and therefore won’t be searchable.

Virtual Scrolling is ideal (when it works) in a number of situations:

  • an undefined and possibly enormous list of items (approximately greater than 5k, but it’s highly dependent on the complexity of each item)

  • infinite scrolling of items

2. Manual Rendering

One of the options I’ve tried to speed up a large list of items is manual rendering using Angular’s API rather than relying on *ngFor.

We have a simple ngFor loop template:

    <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>

I’m using a benchmark inspired by js-frameworks-benchmark to calculate the rendering of 10000 simple items.

The first benchmark run was done with a simple, regular *ngFor. Here are the results: scripting took 1099ms and rendering took 1553ms, 3ms painting.

By using Angular’s API, we can manually render the items.

    <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>

The controller’s code changes in the following way:

  • we declare our template and our container
    @ViewChild('itemsContainer', { read: ViewContainerRef }) container: ViewContainerRef;
    @ViewChild('item', { read: TemplateRef }) template: TemplateRef<*any*>;
  • when we build the data, we also render it using the ViewContainerRef createEmbeddedView method
    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
        });
      }
    }

Results show a modest improvement:

  • 734ms time spent scripting, 1443 rendering, and 2ms painting

In practical terms, though, it’s still super slow! The browser freezes for a few seconds when the button is clicked, delivering a poor user experience to the user.

This is how it looks like (I’m moving the mouse to simulate a loading indicator 😅):

Let’s now try Progressive Rendering combined with Manual Rendering.

3. Progressive Rendering

The concept of progressive rendering is simply to render a subset of items progressively and postpone the rendering of other items in the event loop. This allows the browser to smoothly and progressively render all the items.

The code below is simply:

  • we create an interval running every 10ms and render 500 items at once

  • when all items have been rendered, based on the index, we stop the interval and break the loop

    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);

Notice that the number of items rendered and the interval time is totally dependent on your circumstances. For example, if your items are very complex, rendering 500 items at once is certainly going to be very slow.

As you can see below, the stats look certainly worse:

What’s not worse though is the user experience. Even though the time it takes to render the list is longer than before, the user can’t tell. We’re rendering 500 items at once, and the rendering happens outside of the container boundaries.

Some issues may arise with the container changing its size or scroll position while that happens, so these issues need to be mitigated in a few cases.

Let’s see how it looks like:

Final Words

The above techniques are certainly useful in some situations and I’ve used them whenever virtual scrolling was not the best option.

With that said, for the most part, virtual scrolling using a great library like Angular’s CDK is definitely the best way to tackle large lists.

If you need any clarifications, or if you think something is unclear or wrong, do please leave a comment!

I hope you enjoyed this article! If you did, follow me on Medium, Twitter or Dev for more articles about Software Development, Front End, RxJS, Typescript and more!

Posted on by:

gc_psk profile

Giancarlo Buomprisco

@gc_psk

UI Consultant, Maker & Technical Writer. I write about JS, TS, Rx, Angular & all things Front End 🇮🇹🇬🇧 Follow me on Twitter: https://twitter.com/gc_psk

Angular

This is where we write about all things Angular. It's meant to be a place for Angular community and people interested in Angular and the Angular ecosystem.

Discussion

markdown guide
 

Hey @gc_psk , Great article!

I had doubt in one thing, in your code for Progressive Rendering, somewhere around line 10, you have this condition:

if (n >= length) {

but I couldn't see any variable with name of length. Can you please check?

 

Hi Dharmen, length is passed as an argument. It represents the length of the list being rendered :)

 
 

One other way:

Don't render 1000 items.
Render only first 100 and add a load more button. If the user wants to load more then they need to click on the button and render the next 100. Simple and easy.

 

Pagination or your solution are definitely other viable ways - but present the same usability limitations as virtual scrolling.

In my case, this was not an option: I have to render thousands of fonts within a dropdown. No other solution has a "click here to load more fonts" button - so I'd be at a disadvantage. This would definitely be a UX limitation for my users.

At the end of the day:

  • do you need to display everything? The solutions above may work
  • do you not? Great! Pagination and Load More could definitely be a solution too
 

You can replace the load more button with a Visibility trigger or a component that calls a callback when the element is in view port, Then, you will trigger a load more action automatically.

 

Great post! I'd add that you can improve performance (especially when filtering / search) is involved by providing a trackBy function. This way you don't need to rerender DOM nodes which have not been changed.

 

Awesome article! Literally just implemented this and it works! No I can render quickly a MASIVE list! Thank you for the article! :)

 

Really great to hear :)

Also check this one out: dev.to/angular/angular-async-rende...

It's probably less performant but it helped me in various places :)