Definition
This is the technique used when you want to prevent the browser from slowing down while scrolling a long list of thousands of items. The key is that even with thousands of data items, you keep only the small number of DOM (the tree of document elements the browser handles) rows currently visible on screen.
To be precise, list virtualization (windowing) is a UI technique that, in a list with hundreds to thousands of items, renders to the DOM only the rows visible in the current viewport (the region actually visible on the browser screen) (or scroll container), and repositions·swaps rows according to scroll position to keep memory usage and layout·paint cost constant. Whereas infinite scroll controls "how much data to load," virtualization limits "which of the loaded data goes into the actual DOM."
Why it matters
Putting every item in the DOM at once makes the initial paint·scroll·reflow (the browser task of recomputing element positions·sizes) cost grow in proportion to the item count. With infinite scroll alone, the number of loaded items grows as the user scrolls, but the DOM node count grows along with it, so after long scrolling the browser can slow down. Virtualization keeps the DOM to a small number of rows near the viewport even when the data array is long. From the critical-rendering-path perspective, it has the effect of limiting the number of nodes subject to layout/paint.
How it works
- Flatten row model: nested UI like year·month groups often does not fit the virtualizer input. Flatten it into a one-dimensional row array like
month-header|item, then manage it withcountandgetItemKey. - Window virtualizer: if the whole page is the scroll container use
useWindowVirtualizer; if inside a fixed-height panel useuseVirtualizer. If there is a sticky top header·filter, align the offset withscrollMargin. - Variable height: estimate the initial height with
estimateSize, then correct the actual height after render withmeasureElement. A wrong estimate causes scroll jumps. - Combining with infinite scroll: at the scroll end, increase
visibleCount(or page) and update the virtualizer'scounttogether. Control data exposure and the DOM ceiling separately. - SSR + virtual: if you use only the virtualizer for SSR (Server-Side Rendering, where the server builds HTML in advance and sends it)/initial render, off-viewport rows can be missing from the HTML. If first paint·SEO·hydration (the process where React attaches events to server-built HTML to make it actually work) matters, guarantee a static first page via SSR, then switch to the virtual list after
useLayoutEffect(ssr-hydration-mismatch).
Practical application
- The server serializes only the metadata needed for the list and excludes the body (single-source-of-truth-content-metadata). The client progressively reveals with
visibleCount. - To keep month headers from being cut off on slice, add logic that keeps only the
month-headercorresponding to the set of visible TIL slugs. - When a condition like a tag filter changes, reset the internal state with
<VirtualList key={filter}>. - If items number in the tens of thousands or more, consider API paging (
/api/items?offset=&limit=) instead of the full client array.
Trade-offs
| Choice | Pros | Cons |
|---|---|---|
| No virtualization | Simple to build, easy SEO·a11y prediction | Perf degrades as item count grows |
| Virtualizer-only SSR | Simple code | Items may be missing in first HTML·hydration |
| Static gate + virtual | Stable first paint·hydration | Cost of maintaining two render paths |
| Library (e.g. TanStack Virtual) | Variable height·window scroll support | Learning·bundle cost |
When not to use it
- When there are a few dozen items or fewer and DOM cost is negligible.
- When the entire list must be read at once by SEO·print·screen readers (virtualization skips rows not in the DOM).
- When row heights are extremely irregular and you use only estimation without measurement correction (scroll jumps).
Common mistakes
- Layering a nested
<section>structure directly onto the virtualizer, making slice·keys·measurement complex → switch to a flatten row. - Using only infinite scroll and omitting virtualization, so the DOM keeps growing as loaded items increase.
- SSR-ing only the virtualizer, so the first-view HTML is empty or hydration mismatches.
- Using only
estimateSizeand omittingmeasureElement, so scrolling jumps on variable-height cards.
Related concepts
- ssr-hydration-mismatch — the principle of deferring client-only UI (virtual switch) until after mount·layout effect.
- critical-rendering-path — the effect of DOM-size reduction on initial·scroll render cost.
- single-source-of-truth-content-metadata — the pattern of serializing only list metadata on the server to reduce the RSC (React Server Components, React components that run only on the server) boundary payload.