pattern · perceived-performance
List virtualization
also called windowing (community), virtual scrolling (community), virtualized list (community), virtual list (ant-design), row virtualization (tanstack-table)
Rendering only the rows currently in view and a small buffer around them, while spacers preserve the scrollbar of the full list.
The list has ten thousand rows and the document has fourteen. A virtualized scroller watches its own scroll position, works out which slice of the data that position lands on, and renders that slice plus a few rows either side as a cushion. Everything else is held up by a spacer sized to what the full list would have measured, which is what keeps the scrollbar honest: the thumb is the right size, the scroll distance is the real one, and the reader never learns that the rows they are heading towards do not exist yet. Most implementations assume a fixed row height, because that assumption turns “which rows are visible” into one division.
The reason to reach for it is that browsers are fast at scrolling and slow at holding ten thousand of anything. Layout, paint, memory, and event listeners all scale with the node count, and a table with a few columns of markup per row hits the wall long before the data does. Virtualization decouples the two: the DOM cost becomes a constant set by the viewport rather than by the dataset, which is why it turns up under every large data grid, log viewer, chat history, and file tree.
It is easy to confuse with the patterns that decide when data arrives, and the line is worth being exact about. Infinite scroll, load more, and pagination are all answers to when the next batch is fetched; virtualization is an answer to whether the rows you already have exist in the document. They compose freely, and usually do: an infinite feed that never virtualizes gets slower the longer it is read, and a virtualized list still has to get its data from somewhere. A paginated table can virtualize a single page, and often should not, since a page small enough to read is small enough to render.
The costs are real and mostly land on the reader. Flick fast enough and the scroller
outruns its own rendering, showing a band of nothing where rows should be, which is why
serious implementations overscan generously and fill the gap with a
skeleton screen rather than blank space. Rows that are not in the
document cannot be found by the browser’s own find-in-page, cannot be linked to with a
fragment, and cannot hold focus: unmounting the row a keyboard user is standing on
throws focus to the body unless the code catches it. Assistive technology needs to be
told what it cannot see, with aria-setsize and aria-posinset on each row, since the
accessibility tree only ever holds the window. Variable row heights require measuring
and caching, and a wrong guess makes the scrollbar lie and triggers
scroll anchoring corrections mid-scroll. None of this is worth
paying under a few hundred rows, where content-visibility: auto gets much of the
benefit for one line of CSS and none of the bugs.
Which word?
| If you want | say |
|---|---|
| a huge list renders only the rows you can see | list virtualization |
| only twenty rows of ten thousand are in the DOM | set size and position |
| content is fetched only once it is nearly on screen | lazy loading |
Related
See also: Infinite scroll