Skip to content

Instantly share code, notes, and snippets.

@andyearnshaw
Last active April 12, 2024 09:39
Show Gist options
  • Save andyearnshaw/5cc3b6e158879481515abef8c20d92a7 to your computer and use it in GitHub Desktop.
Save andyearnshaw/5cc3b6e158879481515abef8c20d92a7 to your computer and use it in GitHub Desktop.
css-proposal-virtual-scrolling

Abstract

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.

Use cases

  • 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.

Background

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:
    <div class="container">
      <div class="sizer"></div>
      <div class="content"></div>
    </div>
    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.

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.

Existing implementations

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.

Proposed solution

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:

  1. If the element already has a virtual scrollbar attached on that axis, throw
  2. Return a new VirtualScrollbar instance to the caller
  3. When the virtual scrollbar's scrollMax property is set; a. If the virtual scrollbar's axis property is set to "horizontal", update the element's scrollWidth value with the new value for scrollMax b. Else update the element's scrollHeight value with the new value for scrollHeight
  4. If the virtual scrollbar's axis property is set to "horizontal", ignore any content changes that affect the element's scrollWidth
  5. If the virtual scrollbar's axis property is set to "vertical", ignore any content changes that affect the element's scrollHeight

Example

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>

Frequently Asked Questions

  • Why not ask the browser vendors to increase their maximum element sizes?
    Already done:

    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 like element.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.

@Akxe
Copy link

Akxe commented Jun 28, 2019

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.

@andyearnshaw
Copy link
Author

@Akxe thanks for your comments, they're very helpful. However, at this stage there isn't much point refining this document until there's some kind of acceptance from a web standards group who want to help flesh this out (the actual approach might look nothing like this proposal).

I'd recommend you adding your use cases and comments to WICG/webcomponents#791 or w3c/csswg-drafts#3397.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment