Virtualization
Rendering 5,000 tiles would mean ~15,000 DOM nodes. useVirtualGrid keeps only what's on
screen, so the DOM stays at a few dozen nodes and scrolling holds 60 FPS.
Spatial bands
Items are bucketed once into fixed-height bands (≈ one viewport tall). This handles
masonry's non-monotonic y across columns, which a single binary search can't.
function buildBands(positions, bandHeight) { // O(n), once per layout
for (const [i, p] of positions.entries()) {
const start = Math.floor(p.y / bandHeight);
const end = Math.ceil((p.y + p.height) / bandHeight) - 1;
for (let b = start; b <= end; b++) bands[b].push(i);
}
}A scroll position maps to an inclusive band range; the union of those buckets is the visible set — O(1) per frame.
No state churn on scroll
Scroll position is read from refs inside a requestAnimationFrame-throttled handler.
React state updates only when the visible band range changes (crossing a boundary), never
per frame. A spacer of exactly containerHeight reserves the scroll area → zero CLS.
const onScroll = rafThrottle(() => {
const [start, end] = bandRange(scrollTop, viewportH, overscan, bandHeight);
if (start !== prev.start || end !== prev.end) setVisible(collect(start, end));
});Overscan & adaptive overscan
A base 500px overscan renders just outside the viewport so tiles are ready before they appear. Adaptive overscan widens it in proportion to scroll velocity, then shrinks when idle — tune both live on the benchmark.
Why not an IntersectionObserver per band? Band sentinels would add hundreds of DOM nodes and defeat the "< 100 nodes" goal. Scroll-math + bands is O(1)/frame and keeps the DOM tiny. IntersectionObserver is still used where it shines — infinite-scroll triggering.
GPU compositing
Visible tiles are absolutely positioned with transform: translate3d(x, y, 0). Position
changes (resize / engine swap) animate via transform only; width and height snap instantly —
layout properties are never animated.
Bi-directional scroll & anchoring
The feed is a two-ended stream: scrolling to the bottom appends the next page, scrolling
up into the upper margin prepends the previous page (useInfiniteImages.loadEarlier).
Prepends are gated on upward motion (delta < 0) once the scroll passes a 1200px
threshold — so the first downward pass past that line doesn't spuriously prepend ahead of the
visual baseline. Prepending shifts every coordinate, so without compensation the viewport
would jump (scrollHeight grows above the read position).
Anchoring fixes this with a pure, testable pair in @repo/utils:
// Capture *before* prepending (positions still describe the old layout):
const anchor = pickAnchor(positions, window.scrollY); // topmost visible tile + offset
// …prepend applies, positions re-laid…
const y = restoreScrollY(newPositions, anchor); // "tile at same offset again"
window.scrollTo(0, y);useScrollAnchor({ positions, prependCount }) wires this into a layout effect, so the restore
happens before paint — the browser never displays the displaced frame. Because the restore
compensates only the prepend, the user's own scroll position is preserved exactly: an E2E test
holds the prepend request, measures the pre-restore scroll delta, then asserts the anchored
tile lands within 80px of where the user's own scroll put it. Sealed rows keep their exact
coordinates and stable object identity across prepends, so the predicate re-layout is O(delta)
and no visible tile moves.