Build your Own Virtual Scroll - Part I - DEV

 
notion imagenotion image
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:
notion imagenotion image
notion imagenotion image

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:
notion imagenotion image
As the user scrolls, we recalculate which nodes to add or remove from the screen:
notion imagenotion image
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.
notion imagenotion image

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:
notion imagenotion image
The total number of visible nodes is derived from the viewport's height, divided by row height, and we add the padding as well:
notion imagenotion image

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.