What this post covers
On pages where items keep growing — like a blog TIL list — "render everything at once" makes DOM and initial cost large. At the same time, the first screen must have real cards in the server HTML to help SEO and LCP. This organizes into one task the distinction between infinite scroll and virtualization, and the SSR-first-slice → post-hydration virtual-list switch pattern.
1. flatten → SSR first slice → virtualizer switch
Background (concepts)
- Infinite scroll: when you hit the scroll end, load more data (or raise the exposure ceiling).
- Virtualization (windowing): mount only visible rows in the DOM to keep the DOM-count ceiling.
- Hydration: the process of React attaching events and state to server HTML.
What the situation was
As the blog TIL list grew, the DOM and initial-render cost of the "render everything at once" approach grew. Because it was a nested structure grouped by year/month, it was also awkward to feed straight into the virtualizer.
Meanwhile the first visitor needed HTML with real cards, not an empty shell. SSR-ing only the virtualizer can drop out-of-viewport rows from the HTML.
There were four goals — include the first 10 real cards in the server HTML (first paint), raise the exposure ceiling by 10 on reaching the end (infinite scroll), keep only the visible range in the DOM (DOM ceiling, TanStack Virtual), and flatten nested year/month → 1D rows (month-header / til-item) (structure).
Core work
- flatten: flattened the year/month nested structure into
month-header/til-item1D rows and connected it to TanStack Virtual. - SSR: the server serialized only the list metadata and passed
initialRows(first 10) and the fullrowsto the client. - Static gate: before hydration, a static list component guaranteed the first slice in the HTML.
- Switch: switched to the virtual list after
useLayoutEffect. - Infinite scroll: combined with infinite scroll that raises
visibleCountby 10 at the scroll end.
Takeaways
- Infinite scroll ≠ virtualization — the former limits the amount of data to load/expose, the latter the number of DOM rows.
- Variable-height cards jump on scroll with
estimateSizealone → needmeasureElementcorrection. - SSR-ing the virtualizer alone can leave the first HTML incomplete, so when first paint matters, a static gate pattern is better.
- Follow-up: if items grow very large, reduce passing the whole client array via API paging; verify
/tilfirst HTML and scroll with e2e.
→ List virtualization and windowing · SSR hydration mismatch