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.
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:
- Pass input array as
- Get the items to be rendered from
- Define an item and use
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
startindex 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.
let start = Math.floor(scrollTop / scrollHeight * itemCount / itemsPerRow) * itemsPerRow;
let end = start + viewHeight / childHeight * itemsPerRow
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:
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:
childWidth and used them in the calculation whenever they were defined.
Bingo! We have now a functional virtual-scroll component.
This component is ready for API integration as well.
change event is fired every time
end index change. You could tap onto this event to load more items at the end of the scroll using an API. See below.
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.