跪拜 Guibai
← All articles
Frontend · JavaScript · Performance Optimization

Virtual Scrolling Isn't Always Faster: Benchmarks at 10K and 100K Rows

By Mh ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

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.

Summary

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.

Takeaways
At 100,000 rows, the normal list spent 5,721 ms in scripting and 5,763 ms in rendering; the virtual list spent 592 ms and 710 ms.
At 10,000 rows, the normal list's scripting time dropped to 221 ms, while the virtual list's scripting time rose to 710 ms due to per-scroll calculations.
Virtual scrolling keeps DOM node count around 20 regardless of total data size, using a spacer div to maintain correct scrollbar proportions.
A buffer of 6 rows above and below the viewport prevents blank areas during fast scrolling but increases the rendered DOM slice.
The `moveY` translateY offset is essential: without it, sliced rows render at the top of the container instead of their correct position in the full list.
Fixed-height virtual lists break when actual row height deviates from the assumed `rowHeight` constant; the position error grows with scroll distance.
Below roughly 10,000 simple rows, a plain rendered list can outperform virtual scrolling because it avoids per-frame JavaScript recomputation.
Virtual scrolling pairs naturally with backend pagination: the frontend reduces DOM, the backend reduces payload size and memory pressure.
Conclusions

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.

Concepts & terms
Virtual Scrolling
A technique that renders only the subset of list items currently visible in the viewport (plus a small buffer), replacing the full list with a spacer element that preserves the correct scrollbar height.
moveY / translateY offset
The CSS transform applied to the visible row container to shift it downward by the pixel height of the rows that were sliced away, so rendered rows appear at their correct scroll position rather than at the top of the container.
Buffer rows
Extra rows rendered just above and below the visible viewport so that fast scrolling does not expose blank areas before the next slice of data is computed and painted.
rowHeight constant
The assumed pixel height of every row in a fixed-height virtual list. If the actual rendered height differs, the scroll offset calculation drifts, causing items to appear at wrong positions.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗