Building Virtual Scroll for Angular 2

Virtual repeat for Angular 2— enabling you to display “infinite” list without performance issues.

Virtual scroll is a tricky component to build, but essential for performance critical web applications that handles large data. If you want to display very large lists, filtering is a friendly and primary way to tame the data. However, if data is still large enough to cause performance issues due to the uncontrolled growth of DOM elements and operations — virtual scroll can help.

Before We Start

If you have already used angular-virtual-repeat module for Angular 1, you know what I’m talking about. There were some excellent alternatives as well, including some wrappers of jQuery grids, but nothing was providing Angular 2 pattern and multi-column support. So I decided to build one.

A fully functional module is available at npm. So if you want to just jump in and start, please check angular2-virtual-scroll.

npm install angular2-virtual-scroll

What is Virtual Scroll?

Let’s say you have an infinitely growing list of data like Facebook posts or tweets; there are several approaches to render such a list in an efficient way.

The first approach is to load and display data in chunks and allow user to navigate through the chunks using buttons (eg: google’s search page). This was the best way known for many years. However, user needing to click a button repeatedly to navigate, especially when the list is large, brings down the experience.

The second approach — an improved version of the first approach — is to remove the pagination buttons, but keep appending items — in chunks, of course — to the list as you reach the end of the scroll. By doing so you would improve user experience. But an infinite growth of DOM elements will sooner or later cause the browser to run out of resources, forcing it to slow down and eventually crash.

The third approach — virtual list — is to display a small subset of records just enough to fill the viewport and keep changing them as the user scrolls. By keeping the number of DOM elements constant, this approach provides an efficient way to render a virtually infinite list at the cost of a tiny number of DOM nodes and fewer DOM operations. This is sometimes called Virtual Repeat in angular world. Here is my journey how I build one myself.

Usage

Besides the complexity of the component, it was very important to define a simple and straight forward way of usage. So I decided to go with the following.

Only three things to remember:

  1. Pass input array as items
  2. Get the items to be rendered from update event
  3. Define an item and use *ngFor for repeats.

Building the component

The component consists of 3 layers.

A padding layer (bottom layer) that will keep the appropriate scroll height based on two factors: number of items and height of an item.

The middle layer, positioned absolutely above the padding layer and translated vertically (translateY) to place in the appropriate scroll position. This layer contains list items.

And finally a wrapper layer <virtual-scroll> (viewport layer) with overflow: hidden; overflow-y: auto;

Here is how it’s written in Angular 2 way:

The real deal

Now comes the difficult part. Calculations! We need to calculate 5 primary parameters.

  • start — index of the first item that’s to be placed in the viewport based on current scroll position; start = scrollTop / scrollHeight * itemCount.
  • end — index of the last item that’s to be placed in the viewport based on current scroll position; end = start + viewHeight / childHeight.
  • viewportItems — The items to be shown; viewportItems = items.slice(start, end)
  • scrollHeight — the total height of the scroll. scrollHeight = childHeight * itemCount.
  • topPadding — the vertical translation for the middle layer; once we know start index it’s easy to calculate. topPadding = childHeight * start

Putting it all together.

We have some more calculations to make. These parameters are part of our primary calculations.

Adding Multi-Column Support

Adding multi-column support adds to the complexity of the calculations. Number of items per row (itemsPerRow) need to be factored in.

  • start index — let start = Math.floor(scrollTop / scrollHeight * itemCount / itemsPerRow) * itemsPerRow;
  • end index — let end = start + viewHeight / childHeight * itemsPerRow
  • viewportItems —no-change; viewportItems = items.slice(start, end)
  • scrollHeight = childHeight * itemCount / itemsPerRow
  • topPadding = childHeight * start / itemsPerRow

Items with variable size

As you would have noticed all of these calculations are based on two important parameters: childHeight and childWidth. When a list contains items with variable size, make all the calculations based on the smallest size. To allow developers to define the smallest size, I added two inputs: childHeight and childWidth and used them in the calculation whenever they were defined.

@Input()
childHeight: number;
@Input()
childWidth: number;

And finally…

Bingo! We have now a functional virtual-scroll component.

API Integration

This component is ready for API integration as well. change event is fired every time start or end index change. You could tap onto this event to load more items at the end of the scroll using an API. See below.

See Demo

Click below to see this component in action.

Challenges and Future Work

Container size is another important parameter for the scroll calculation. Any change in container size could possibly break virtual scroll, unless refresh() function is called manually. Inherently, it is difficult or rather expensive to identify the change in container size if it is not caused by document.window (eg: dropdown open/close action). So the auto refresh feature is currently parked aside until Resize Observer is fully implemented.

There are one or two minor details to these calculations to make it perfectly work for corner cases. It is outside the scope of this article. Please check my GitHub repository for the final version.