This is a 2 part series:
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
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.
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.
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:
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.
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:
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:
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
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:
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.