Virtual Scrolling Isn't Always Faster: Benchmarks at 10K and 100K Rows
Virtual scrolling is often treated as a default optimization, but these numbers show it can be a net negative below ~10,000 simple rows. Teams shipping admin panels, dashboards, or log viewers should measure before adopting it; the per-frame compute cost is real and can regress scripting time on mid-sized lists.
A side-by-side implementation of a normal scrolling list and a fixed-height virtual list in Vue 3, both fed 100,000 rows of mock order data. The virtual list renders about 20 DOM nodes at any time by slicing the data array and using a spacer element to preserve scrollbar height, with a buffer of 6 rows above and below the viewport to prevent blank flashes.
Chrome Performance recordings tell a clear story. At 100,000 rows, the normal list burns over 5.7 seconds in scripting and rendering, while the virtual list finishes in under 600 ms. Drop the dataset to 10,000 rows, and the normal list becomes smooth, with scripting time actually lower than the virtual list's constant recomputation of start, end, and translateY offsets.
The crossover point sits around 10,000 simple rows. Virtual scrolling wins decisively on large datasets and complex row DOM, but it adds a per-frame JavaScript tax that can make it slower than plain rendering when data is modest and rows are cheap to paint.
The 10,000-row crossover is specific to simple DOM; a row with images, badges, or nested components would shift the break-even point much lower, making virtual scrolling worthwhile sooner.
Vue's template-to-render-function compilation means even a 'static' `v-for` list incurs scripting cost proportional to data length — creating 100,000 vnodes and patching them is pure JavaScript work, not just DOM work.
The `buffer` parameter is a direct trade-off between smoothness and overhead: too small and fast scrolls flash white; too large and you erode the very benefit virtual scrolling provides.
Virtual scrolling's real value is not just speed but memory: keeping 20 DOM nodes instead of 100,000 prevents the browser from ballooning memory on long-lived pages like monitoring dashboards or infinite feeds.