Virtual List: Overcoming the 16,777,200px Limitation of Chrome

Summary
  • Many virtual list libraries like react-window, svelte-virtual-list, and TanStack's Virtual cannot handle the total height larger than 16,777,200px due to Chrome's limitation. If you have a list of 1M rows where each row is 28px, you won't be able to scroll pass the 559,185th row.
  • I've implemented my own virtual table for Svelte (a single file of 224 lines!) that can surpass the Chrome limitation. You can install it by running npm install svelte-virtual-table-by-tanin. You can see the demo of 1M rows (~28M pixels) here: link
  • I've solved it by compressing the true space to fit 16,777,200px.
  • Later I've found that SlickGrid (a pure JS library) has solved the 16,777,200px Chrome limItation and seems to use the same approach that I did.

For people who might be unfamiliar with virtual list, it renders a large list of items by avoiding generating one div for each item; instead it renders only the divs that you can see. As you scroll, it removes the unseen divs and renders the newly seen divs. This saves an enormous amount of memory.

I use react-window for Superintendent.app (Query CSV file using SQL). I use svelte-virtual-list in Backdoor (Postgres Data Querying and Editing Tool that you can embed into your JVM app).

For some time, I've noticed that both virtual lists cannot go beyond 16,777,200px in height. This is because Google Chrome doesn't allow a div (or its paddings) to be taller than 16,777,200px.

TanStack's Virtual

Notice the 2.8e+07px div's height on the bottom left panel and the 16777200 computed height on the bottom right panel. Because Chrome auto-adjusts a 28,000,000px div into a 16,777,2000px div.

While the limit is high, it's really not that high. One million rows would already exceed it assuming each row is a single line (~20px).

You can try a few famous libraries with 1,000,000 rows (28px / row, ~28M pixels in total height) here: react-window (16.8K Github stars), svelte-virtual-list (written by the Svelte creator), and TanStack's Virtual (6.4 Github stars). You can scroll to the bottom most position and will see that all of them only scroll to the ~559,185th rows.

As a former Google Chrome engineer, I've decided to embark on a journey to overcome this limitation.

The solution

💡
I've published my own virtual table for Svelte. You can install it by running npm install svelte-virtual-table-by-tanin. You can see the demo of 1M rows (~28M pixels) here: link

First of all, we know that the 16,777,200px Chrome limitation cannot be changed. There's no feasible way to change that.

One workaround is to map every pixel within 16,777,200px (aka the scrollable space) to your true space which might be much bigger. Let's say: if the true space is Z px, and the scroll top is at P px, then the true scroll top is at Z * P / 16777200. That's the basis of it.

Next, we prepare our scrollable space that looks like below:

<div style="overflow: auto;height: 400px;"> // viewport
  <div style="height: {SCROLLABLE_SPACE}px; position: relative;">
    <div style="position: absolute; top: {scrollTop - deltaTop}px">
     <!-- put your rows here -->
    </div>
  </div>
</div>

deltaTop is needed because sometimes scrollTop might fall in the middle of a row.

Assuming deltaTop is 0, this means your rows always show starting at the top of the viewport because top is equal to scrollTop.

Then, when scrollTop changes (you can listen to the change using the onscroll event), we translate scrollTop into trueScrollTop and render the items at the trueScrollTop position. And that's it.

This works pretty well but there are 2 details we need to take care of:

1: Over-scrolling

It turns out Chrome allows over-scrolling by ~20px. When an over-scroll occurs, it'll fire another scroll event with the max scroll position immediately. This would cause an infinite flickering if we were to drag the scroll thumb to the bottom-most position and hold it there.

It flickers!

What happens underneath is that the total height of the scrollable-space div is extended by 20px. Then, it immediately contracts by 20px because the scroll top will be automatically adjusted back. The flickering would go away if we set its overflow to hidden, but this isn't the right solution because it would hide the horizontal overflow as well. And if you've played with overflow before, you probably know that overflow-y: hidden; overflow-x: visible; isn't a valid combination, and I have no idea why.

Since this kind of flickering doesn't occur in a normal-sized-area scrolling situation, we want to simulate the same experience here.

To handle it, we need to detect whether the rendered items exceeds the scrollable space (i.e. 16,777,200px). If so, then we can simply render items in a way that ensures the last item is at the bottom-most position. This would completely eliminate the flickering issue.

No flickering

2: Searching which item corresponds to trueScrollTop can be slow

Using a brute-force algorithm to search for such item would need to iterate through millions of items.

The scrolling becomes much smoother if we use a binary search to do this, which reduces from O(n) to O(log n).

Finally, I've got to implement a binary search outside of an interview. Admittedly, I actually used AI to implement it but I did review and ensure it was correct!

Alternatives considered

There were 2 alternatives that I tried. They are inferior one way or the other to the solution described above.

Custom scrollbar

Implementing custom scrollbar would overcome the limitation because we would need to make an extremely large <div> in order to get the native scrollbar's size to be accurate.

However, the downside is that it's infeasible to match all the appearance and interactions of a native scrollbar. Some aspect can be matched like the color and size. But other aspects like the animation, the placement in regard to the horizontal scrollbar, the scroll speed (which is not constant by the way), and the interaction with touch-based screens aka ipad are extremely tricky to implement.

I implemented this a bit then later deemed it as infeasible. The amount of code and testing on different devices would have been insane.

Using padding (or divs as paddings)

The approach is more complex because we are changing 3 numbers (padding top, content top, and padding bottom) on every scroll event. The padding top and bottom live in the compressed space but the content has to live in the actual space. The space mapping is more complex due to the non-linear nature of it.

In addition, what I've encountered was that sometimes the container's height changed back and forth by 1px because we adjust padding-top, content size, and padding-bottom. I don't why it impacts the container's height but it does. This would cause a full refresh because the total height was changed.

Moreover, the padding-top, content size, and padding-bottom are fractional values and sometimes result in different total heights. This would contribute to flickering.

Compared to the proposed solution, which only change the top position but doesn't change the scrollable height, the approach is simpler and avoid the flickering issue.

This approach seems to be fine at a smaller scale since svelte-virtual-list and react-window use this approach.

Parting thoughts

For virtual list creators out there, thank you for making the libraries. I've learned a lot. Special thanks to Rich Harris (the Svelte creator!) who created svelte-virtual-list, which was a single 168-line file. It was small enough for me to understand and got me started in improving the virtual list. I hope this gives an idea how to improve your virtual list because 16,777,200px isn't really that much in this day and age.

The virtual table for Svelte that I've built can be installed using npm install svelte-virtual-table-by-tanin. Its source code can be found here: https://github.com/tanin47/svelte-virtual-table-by-tanin. My implementation is a single file of 224 lines! You can see a demo with 1,000,000 rows (~28,000,000px in height) here: link

After I built my own virtual table, I surveyed a bunch of virtual tables libraries mentioned earlier and built a working example for each of them: react-window, svelte-virtual-list, and TanStack's Virtual. It turns out SlickGrid (a pure JS library) seems to use the same approach described here. You can see a working example here: link. If you inspect the elements, you'll see that the scrollable div's height is kept at 16,777,200px! And, for a brief moment, I thought I was the first person who invented this 🤣

Subscribe to tanin

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe