Part V β The Craft Β· Lesson 10.7
Infinite Scroll
In one lineA sentinel element triggers the next page, and virtualization stops the DOM from growing forever.
Where you've seen itA social feed that scrolls beautifully for two minutes and then starts stuttering on every frame.
Diagram
THE SENTINEL β an empty div that says "load more"
βββββββββββββββββββββββββββ
β item 18 β
β item 19 β viewport
β item 20 β
βββββββββββββββββββββββββββ€
β item 21 β β rendered, below the fold
β βββ sentinel βββ β β IntersectionObserver fires here
βββββββββββββββββββββββββββ rootMargin: '400px' means it
fires BEFORE the user arrives
No scroll event listeners. No getBoundingClientRect in a loop.VIRTUALIZATION β render the window, not the list
WITHOUT WITH
ββββββββββββββββ ββββββββββββββββ
β 2,000 <li> β β spacer div β height = 1,200 Γ 80px
β in the DOM β ββββββββββββββββ€
β β β item 41 β β ~15 nodes rendered
β memory: 1GB β β item 42 β
β scroll: 12fpsβ β item β¦ β
β β ββββββββββββββββ€
β β β spacer div β height = remaining
ββββββββββββββββ ββββββββββββββββ
memory: flat Β· scroll: 60fpsTHE STATE MACHINE β every path needs a UI
[idle] ββsentinel visibleβββΊ [loading] ββokβββββΊ [idle]
β² β (append page,
β β cursor = next)
β βββerrorβββΊ [error] ββretryβββ
β β β
β βββemptyβββΊ [end] ββββββββββββ
β "No more results"
βββ guard: never fire while already loadingHow it works
- Use
IntersectionObserveron a sentinel with arootMarginso the next page starts before the user hits the bottom. - Guard against double-firing β the observer can trigger repeatedly while a request is in flight.
- Paginate by cursor, not offset (10.11) β new items arriving at the top will shift offsets and duplicate rows.
- Virtualize past a few hundred items with
react-windowor TanStack Virtual; without it, DOM size is the bottleneck. - Preserve scroll position on back-navigation by caching the loaded pages and the offset (6.9).
useEffect(() => {
const io = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting && !loading && hasMore) loadNext();
}, { rootMargin: '400px' });
io.observe(sentinelRef.current);
return () => io.disconnect();
}, [loading, hasMore]);Trade-offs
| Infinite scroll | Pagination |
|---|---|
| Effortless browsing, mobile-friendly | Deep-linkable, position obvious |
| Footer unreachable | Footer reachable |
| Harder to return to an item | "Page 4" is a real address |
| Needs virtualization to stay fast | DOM stays small naturally |
Accessibility (8.3): announce loaded batches via a polite live region, never move focus on load, and provide a "Load more" button as a keyboard-operable alternative β a keyboard user cannot trigger a scroll sentinel easily.
Interview angle
"Build an infinite feed. What breaks first?"
- The DOM: without virtualization, memory and scroll performance degrade after a few hundred items.
- Offsets: with items being inserted at the top, offset pagination duplicates and skips β use cursors.
- Then the details that get skipped: the double-fire guard, the end state, scroll restoration, and a keyboard-accessible load-more.
Recap
IntersectionObserveron a sentinel withrootMargin, guarded against double-firing.- Virtualize once lists get long β that's the actual scaling limit.
- Cursor pagination, scroll restoration, and a keyboard alternative are what separate a demo from a feature.