The way that most virtualized list components work is that instead of passing a list of elements to render, we instead provide the list with just the number of elements we want to render, how big each element is, and a callback which renders a single item.

The first thing we need to change is how the elements in the list are laid out. Normally we’d create a lot of divs next to each other and let the layout engine stack them up, but now we’re going to be skipping most of the elements. Instead, we’ll position each element absolutely, and force the inner container to be the right height so the scrollbar will still render correctly.

We could wrap each item in a new div to give it the right position and top values. However, most virtualization libraries pass some styles into the item render method, which consumers then need to apply to their own elements. This is slightly more efficient at the cost of being more complicated, so you can decide whether you want to implement this yourself or not.

Now we have a list of items we need to display. We have the index of each item, and we know how tall the items are. We need to calculate the indexes of the items which should be visible. For that, we’ll need three bits of information:

Diagram showing how the inner height, window height and scrollTop relate to each other.

Diagram showing how the inner height, window height and scrollTop relate to each other.

The “inner” height is the total height of the list itself. In our simple case, it’s the item height multiplied by the number of items.
The “window” height is the height of the scrollable area: a window into the full list. This height will depend on the surrounding elements. For now it’s easiest to hardcode it to a specific value; we can look into ways of calculating it later.
scrollTop measures how far the inner container is scrolled. It’s the distance between the top of the inner container and its visible part.

To find the elements which intersect the top and bottom edges of our scrollable area, we divide their pixel position by the height of the elements. We then use Math.floor() to turn the pixel position into a valid element index, and render all the elements between those two indexes:

A minimal virtualized list.

This is a very basic example: there are many potential improvements you might want to make depending on your requirements. Here are a selection:

  • Currently we’re making the assumption that all items in the list are the same size, and passing in a constant itemHeight value. Instead we could make itemHeight accept a callback which queries the item height for each index. This makes calculating the visible items a lot more complicated, though — I guess you’d need to build up an index of pixel offset -> itemindex (probably a binary search tree?). Some libraries offer a second callback which invalidates the saved item heights, so items can change size over time.
  • If you turn on CPU throttling and look at the virtualized example, you may notice it’s possible to scroll to elements before they’re rendered, momentarily leaving ugly-looking white space. Many libraries implement “overscan” to solve this problem, which renders additional elements above and below the visible elements, so they’ll already be visible when they scroll into view.
    This can also be useful if items have some kind of lazy-loading — we can start loading for elements just outside the viewable area so that they’ll be ready to display when the user scrolls down.
  • We’re also assuming that the scrollable area has a fixed size. We still need to know what this size is, but we can get this information automatically using ResizeObserver. Bundling ResizeObserver into a HOC or hook is probably a good idea in React; alternatively you can implement this behavior directly in your virtualized list.

Of course there’s many more tweaks you can make. If you find yourself implementing a lot of these features, you may want to look into using a library yourself (or creating your own!). But being able to build a custom implementation yourself that does exactly what you need is very powerful.
Have fun! :)

References I found useful: