FSD Frontend System Design

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.

  • Time18 min
  • DiagramSentinel + viewport window + virtualization
  • Chapter10 Β· Low Level Design

Diagram

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.
diagram
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: 60fps
diagram
THE 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 loading

How it works

  1. Use IntersectionObserver on a sentinel with a rootMargin so the next page starts before the user hits the bottom.
  2. Guard against double-firing β€” the observer can trigger repeatedly while a request is in flight.
  3. Paginate by cursor, not offset (10.11) β€” new items arriving at the top will shift offsets and duplicate rows.
  4. Virtualize past a few hundred items with react-window or TanStack Virtual; without it, DOM size is the bottleneck.
  5. Preserve scroll position on back-navigation by caching the loaded pages and the offset (6.9).
js
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 scrollPagination
Effortless browsing, mobile-friendlyDeep-linkable, position obvious
Footer unreachableFooter reachable
Harder to return to an item"Page 4" is a real address
Needs virtualization to stay fastDOM 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

  • IntersectionObserver on a sentinel with rootMargin, 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.