DEV Community

Cover image for Build your Own Virtual Scroll - Part I
adam klein
adam klein

Posted on • Updated on

Build your Own Virtual Scroll - Part I

This is a 2 part series:

Part I

Building your own virtual scrolling (windowing) is not as hard as it sounds. We will start by building a simple one where the height is fixed for every row, and then discuss what to do when the heights are dynamic.

Before we dive into the technical details, let's understand the basic concept behind virtual scrolling

Important note:
This is not a "you should build your own virtual scroll" article, merely an article explaining how to do it. I think knowing how things work even if you don't implement them yourself can be very beneficial.

What's a Window?

In Regular scrolling, we have a scrollable container (or a viewport), and content, let's say - a list of items.

The scrollable container has a smaller height than the internal content, thus the browser displays a scroller, and displays only a portion of the content, depending on the scroller position.

You can imagine the viewport as a window, and the content is behind it. The user can only see the part that's behind the window:

Scrolling the container is like moving the content up or down:

Virtual Scrolling

In virtual scrolling, we don't display the entire content on the screen, to reduce the amount of DOM node rendering and calculations.

We "fool" the user to think the entire content is rendered by always rendering just the part inside the window, and a bit more on the top and bottom to ensure smooth transitions.

Notice that we still need to render the content in its full height (as if all the list items were rendered), otherwise, the scroller would be of the wrong size, which leaves an empty space at the bottom and the top:

As the user scrolls, we recalculate which nodes to add or remove from the screen:
loading gif...

You can also imagine this as if walking on a bridge that's currently being built right in front of you and destroyed right behind you. From your perspective, it would feel like walking on a complete bridge, and you wouldn't know the difference.

Let's Do Some Simple Math

For the simple solution, we will assume we know the list length and that the height of each row is fixed.

The solution is to:
1) Render the entire content as an empty container
2) Render the currently visible nodes
3) Shift them down to where they should be.

Let's break down the math of that:

Our input is:

  • viewport height
  • total number of items
  • row height (fixed for now)
  • current scrollTop of viewport

Here are the calculations we make in each step:

Render the Entire Content

As mentioned earlier, we need the content to be rendered at its full height, so that the scrollbar's height is accurate. This is just number of nodes times row height.

Render the Currently Visible Nodes

Now that we have the entire container height, we need to render only the visible nodes, according to the current scroll position.

The first node is derived from the viewport's scrollTop, divided by row height. The only exception is that we have some padding of nodes (configurable) to allow for smooth scrolling:

The total number of visible nodes is derived from the viewport's height, divided by row height, and we add the padding as well:

Shift the Nodes Down

When we render the visible nodes inside the container, they render at the top of the container. What we need to do now is shift them down to their correct position, and leave an empty space.

To shift the nodes down, it's best to use transform: translateY to offset the first node, as it will run on the GPU. This will ensure faster repaints and better performance than, for example, absolute positioning. The offsetY is just the start node times the row height

Example Code

Since the implementation may vary depending on the framework, I've written a psuedo implementation using a plain function that returns an HTML string:

And here is a working example using React:

Performance & Dynamic Heights

So far we've handled a simple case where all rows have the same height. This makes calculations into nice simple formulas. But what if we are given a function to calculate the height of each row?

To answer this question, and further discuss performance issues, you can view part II, in which I'll show how to accomplish that using binary search.

Top comments (11)

Collapse
 
bellindj profile image
Dan Bellinski • Edited

Thanks for a great explanation Adam! I used the technical explanation from the article to modify your ReactJS example to work for a StencilJS web component. It works great and surprisingly not a lot of code!

Collapse
 
olegchursin profile image
Oleg Chursin

Can you share the resulting component, please, Dan? Would love to take a look.

Collapse
 
slidenerd profile image
slidenerd

I have added the Vue.js version codepen.io/zupkode/pen/oNgaqLv It needs a bit of refactoring but thanks to your post it works wonderfully

Collapse
 
zr87 profile image
Zoltan Rakottyai • Edited

It was enlightening! Thanks for sharing!
Attempted to recreate the first part in a barebone Angular example

Collapse
 
adamklein profile image
adam klein

Thanks!
The Stackblitz example doesn't load for me

Collapse
 
alireza30sharp profile image
alireza30sharp

Thanks!
The Stackblitz example doesn't load for me

Collapse
 
tryhardest profile image
tryhardest

Great article. Can't wait to see Pt2 as well. Irregular dynamic row heights without media query obviously make it much more complex, especially when not knowing number and keeping high performance.

Collapse
 
hassam7 profile image
Hassam Ali

let visibleNodesCount = Math.ceil(viewportHeight / rowHeight) + 2 * nodePadding;
Thanks for the nice write up. There is one questions: whats the + 2 for in live above?

Collapse
 
adamklein profile image
adam klein

We add padding above and below the visible viewport, hence we double the nodePadding number - 2 * nodePadding

Collapse
 
bumhyun profile image
bum-hyun

Thanks!
Can I translate this article into Korean?

Collapse
 
adamklein profile image
adam klein

Sure!
Please link back to the original post and send me the link as well so I include it in the original post