What virtualisation actually does
Rendering 50,000 DOM nodes is slow to create, slow to lay out and expensive to keep in memory. Virtualisation renders only what is visible — around twelve rows for a typical viewport — plus a small buffer, and translates them as the user scrolls. Spacer elements preserve the scrollbar so the page still feels like it contains 50,000 rows.
Visually this is a solved problem and several libraries do it well. The part that is routinely wrong is what the DOM now claims about itself, because the DOM genuinely contains twelve items and the user is genuinely looking at item 4,201.
The 'of 12' bug
A screen reader describes a list by its position and size. If the implementation lets those attributes reflect the rendered window, the user hears 'item 4 of 12' while scrolled to the middle of a 50,000-row dataset. The number is not just unhelpful, it is confidently wrong.
`aria-posinset` and `aria-setsize` exist precisely for this: they let you state the true position and total independently of what is in the DOM. Set honestly, the user hears 'item 4,201 of 50,000' — which is what a sighted user infers from the scrollbar.
Focus and recycling
Recycling DOM nodes is what makes virtualisation fast, and it is directly hostile to focus. If the node holding focus is reused for different data, focus stays on a node that now represents something else — the user's context is gone without any indication.
The handling is to track focus by data identity rather than by node, and to restore it when the corresponding item re-renders. Any virtualised list you evaluate should be tested by focusing an item, scrolling far away, and scrolling back.
When not to virtualise
Virtualisation costs complexity, breaks in-page find, breaks Ctrl+F, and complicates printing and copy-paste. For a few hundred rows it is not worth any of that — browsers render a few hundred rows without complaint.
The threshold is roughly where scrolling becomes visibly janky on a mid-range device, and that depends far more on row complexity than row count. A thousand rows of plain text is fine; two hundred rows each containing an avatar, three badges and a chart is not.
Testing it properly
Three checks catch nearly everything. Focus an item, scroll far, scroll back — is focus still on the right item? Turn on a screen reader and navigate — does it announce the true total? Press End — does it reach the actual last item?
Run these on a throttled CPU rather than a fast laptop. Virtualisation bugs are timing-dependent, and a fast machine hides the window where the DOM and the data disagree.
Key takeaways
- Virtualisation renders around twelve rows for a 50,000-row dataset, with spacers preserving the scrollbar
- aria-posinset and aria-setsize must describe the full dataset, or a screen reader announces 'of 12'
- Arrow keys must move through data, not stop at the edge of the rendered window
- Track focus by data identity — node recycling silently destroys focus otherwise
- Do not virtualise a few hundred rows: it breaks in-page find, printing and copy-paste for no gain
- Test by scrolling away and back, checking announced totals, and pressing End — on a throttled CPU
Conclusion
Virtualisation makes the DOM lie about what it contains. Everything hard about it follows from that: the ARIA attributes, the keyboard model and the focus handling all exist to keep the lie invisible to the user rather than confusing to them.
Enjoyed this article?

Vivek Kumar Singh
Technical Expert · Full Stack Cloud Engineer · Tokyo, Japan