Virtual Scrolling is a technique employed by web developers to achieve efficient rendering of components containing large datasets, without creating many thousands or millions of elements that the browser must render and keep in memory. It can also be used to scroll an element which is otherwise unscrollable, because it cannot be overflowed by children in its normal state.
- HTML
canvas
element scrolling. Canvases have no overflow, so you either have to have a very big canvas that overflows its parent (not very efficient), or implement virtual scrolling. Google Sheets is a good example, the entire worksheet is painted on a canvas and it uses custom scrollbars to position the view. - Very large lists. Many companies maintain huge lists containing millions of items, sometimes nested in a tree-like structure. Loading that many items and creating millions of elements could slow the browser to a crawl, so the items are usually loaded as the list is scrolled, and only the number of elements required to fill the view is created.
- Very large data grids. Like with the list example, but 2-dimensional tables consisting of millions of rows and columns. Again, it's not efficient to create so many elements, so virtual scrolling is used to render only what the user can see at any time.
- "Infinite" scrolling. Currently, infinite scrolling solutions just load more content into the same element and increase the scroll height. This gives a weird effect where the user feels like they've scrolled to the bottom, but suddenly there is more scroll space available after new content is appended. Content is also also bound by the maximum element size in the user's browser. Virtual scrolling could enable a very large scroll track from the start, indicating that there is a very large amount of content available. The implementation could also remove elements that have been scrolled out of view, freeing up memory that can be allocated to other resources.
There are two main approaches for implementing virtual scrolling:
- Implement custom scrollbars and update the component based on the position of those scrollbars
- Use a structure like the following to achieve native scrolling:
Where the container is the scrolling element, sizer is resized to get the desired scroll and/or scroll height and content is positioned to take up the full width and height of the container, held in place as the container scrolls using css sticky positioning or JavaScript.
<div class="container"> <div class="sizer"></div> <div class="content"></div> </div>
Each approach has a significant limitation that makes the virtual scrolling implementation a choice between the lesser of two evils:
-
Custom scrollbars are bad UX, and it takes a lot of work to get the style and behaviour to match the browser and OS that the web page is being displayed in. For instance, Windows 10 and Android reduce the content area to paint the scrollbar, but macOS and iOS use an overlay scrollbar that is only visible when you start scrolling. To have a consistent scrollbar across all your components and the web page itself, there is a large amount of work and maintenance involved that should not be asked of developers.
In addition, the browser's native touch scrolling/panning, overflow scroll behaviour (e.g. elasticity) and momentum-based scrolling are not available to elements with custom scrollbars. This means parity with native scrollbars is even more difficult to achieve.
-
The native scrolling approach gets all the style and behaviour goodness included for free, but is limited by the browser's maximum width and height (and scrolling width/height) for an element. In Chrome and Safari it's between about 6.7 million and 32.5 million pixels (depending on zoom level), in Firefox it's around 17.5 million pixels, and in Edge it's around 1.5 million pixels. This limits the size of the data you can display without further virtualizing the scroll values to create a sort of parallax between the native scrollbar position and your view. This means a single pixel change in scrollbar position translates to many pixels scrolled in your rendered view, which could potentially be hundreds or thousands of rows in a list component.
The following open source projects are examples that use native scrollbars, the second implementation method detailed above. They were discovered with a quick search, listed by popularity (number of Github stars). Some of the projects have an issue open detailing the element maximum size limitation also mentioned above.
- https://github.com/bvaughn/react-virtualized ★ 14,024 (uses the parallax technique, see example, set rows to 50,000,000, focus and press the ↓ key)
- https://github.com/mleibman/SlickGrid/ ★ 6,478 (issue)
- https://github.com/NeXTs/Clusterize.js/ ★ 6,068 (issue)
- https://github.com/rintoj/ngx-virtual-scroller ★ 558 (issue)
- https://github.com/stackfull/angular-virtual-scroll ★ 262 (issue)
- https://github.com/tbranyen/hyperlist ★ 185
This document proposes that a VirtualScrollbar
interface be introduced in the CSSOM specification:
enum VirtualScrollbarAxis { "horizontal", "vertical" };
[Exposed=Window]
interface VirtualScrollbar : EventTarget {
readonly Virtualattribute ScrollbarAxis axis;
attribute unrestricted double scrollMax;
};
dictionary VirtualScrollbarOptions {
VirtualScrollbarAxis axis = "vertical";
};
partial interface HTMLElement {
VirtualScrollbar attachVirtualScrollbar(VirtualScrollbarOptions options);
};
When element.attachVirtualScrollbar(options)
is called on an element, the following behaviour is observed:
- If the element already has a virtual scrollbar attached on that axis, throw
- Return a new
VirtualScrollbar
instance to the caller - When the virtual scrollbar's
scrollMax
property is set; a. If the virtual scrollbar'saxis
property is set to"horizontal"
, update the element'sscrollWidth
value with the new value forscrollMax
b. Else update the element'sscrollHeight
value with the new value forscrollHeight
- If the virtual scrollbar's
axis
property is set to"horizontal"
, ignore any content changes that affect the element'sscrollWidth
- If the virtual scrollbar's
axis
property is set to"vertical"
, ignore any content changes that affect the element'sscrollHeight
The following example will render a list of items on a canvas
element, with a vertical scrollbar. The canvas
is repainted on the scroll event:
<canvas id="canvas" width="500" height="300"></canvas>
<script>
const scrollbar = canvas.attachVirtualScrollbar({ axis: 'vertical' });
const ctx = canvas.getContext('2d');
const rowHeight = 32;
const numRows = 100;
const rowsPerView = canvas.height / rowHeight;
let rendering = false;
ctx.fillStyle = 'blue';
ctx.strokeStyle = 'gray';
ctx.font = '12px Verdana';
ctx.translate(0.5, 0.5);
scrollbar.scrollMax = numRows * rowHeight;
function drawRows() {
const { scrollTop, width, height } = canvas;
rendering = false;
ctx.clearRect(0, 0, width, height);
for (let i = 0; i <= rowsPerView; i++) {
const row = Math.floor(scrollTop / rowHeight) + i;
const rowOffset = (rowHeight - (scrollTop % rowHeight)) + ((i -1) * rowHeight);
ctx.strokeRect(0, rowOffset, width, rowHeight);
ctx.fillText(`Item ${ row }`, 10, rowOffset + 22);
}
}
}
canvas.addeventlistener('scroll', () => {
if (!rendering) {
rendering = true;
requestAnimationFrame(drawRows);
}
});
drawRows();
</script>
-
Why not ask the browser vendors to increase their maximum element sizes?
Already done:- someone from the Chrome dev team said this would cause severe memory usage regressions.
- someone from the Webkit dev team said they have no intention of making the wide-spread changes needed.
Even if the teams could have made the necessary changes to increase element sizes, you still have the other downsides mentioned above.
-
The
VirtualScrollbar
interface only has one useful property, why not just add a method likeelement.overrideScroll[Width|Height]
?
This is a basic first definition of virtual scrolling, the bare minimum to cover the majority use case. However, we may want to add more capabilities in the future, such as a cancelable scroll event, an API for scroll snapping, or a scroll position limit to hold the scroll thumb at a maximum value while additional content is loaded. We don't want to limit our options by having the API too basic in its initial definition. -
Given the use cases, isn't it a bit silly to use a scrollbar to scroll through millions of list items?
Yes, but that doesn't mean that the scrollbar shouldn't work properly if someone does use it. A search box is not uncommon but if the user knows the item is just a few mouse wheel rotations away from the top then we should expect those rotations to scroll the correct amount.
There should be option for both axies, also for canvas games, it might be beneficial not to have actual scrollbars but to have events that fire something like, scrollStart that would have information about how far would the scrolling go (mostly on touch), if user would not interact again with the element.