Recipe: Virtualized long list rendering
Render 100k rows without frame drops using intersection observers and DOM recycling.
Problem
Rendering thousands of DOM nodes blocks the main thread, causing jank and memory pressure. Users scrolling a table or feed experience visible lag.
Approach
- Measure a fixed row height (e.g. 48px).
- Calculate visible range from scroll offset + container height.
- Render only visible rows plus a small overscan buffer.
- Use absolute positioning with a spacer div for total height.
- Recycle DOM nodes — never unmount/mount on every scroll tick.
Core loop
const range = {
start: Math.floor(scrollTop / ROW_H),
end: Math.ceil((scrollTop + viewH) / ROW_H)
};
const offsetY = range.start * ROW_H;
// Render items[range.start..range.end] at transform: translateY(offsetY)Caveats
- Variable-height rows require a measured cache.
- Use
will-change: transformsparingly. - Pair with
content-visibility: autofor free browser-level skipping.