mediumReact Native#147

FlatList windowing math

Prompt

The math behind getItemLayout and virtualization, for fixed-height rows:

  1. makeItemLayout(rowHeight) → returns (data, index) => ({ length, offset, index })
  2. visibleRange(scrollY, viewportHeight, rowHeight, itemCount){ first, last } — the inclusive indexes of rows intersecting the viewport, clamped to [0, itemCount - 1]

Solution

function makeItemLayout(rowHeight) {
  return (data, index) => ({
    length: rowHeight,
    offset: rowHeight * index,
    index,
  })
}

function visibleRange(scrollY, viewportHeight, rowHeight, itemCount) {
  if (itemCount <= 0) return { first: 0, last: -1 }
  const maxIndex = itemCount - 1
  const first = Math.min(maxIndex, Math.max(0, Math.floor(scrollY / rowHeight)))
  const last = Math.max(0, Math.min(
    maxIndex,
    Math.ceil((scrollY + viewportHeight) / rowHeight) - 1
  ))
  return { first, last }
}
Mentor's take

This is the arithmetic that makes a 10,000-row list scroll at 60fps, and knowing it cold is what separates "I pass getItemLayout because the docs said so" from understanding virtualization.

Why each piece exists:

  • Without getItemLayout, FlatList must render every row above index N to know where N sits. Async measurement is why scrollToIndex throws without it and why fast scrolls show blanks. Providing offset = rowHeight × index turns layout into O(1) math — no rendering, no measurement, instant jumps. That's the real answer to "why does keyExtractor + getItemLayout matter": identity and geometry are the two things the virtualizer can't guess cheaply.
  • visibleRange is the virtualizer's core loop: floor(scrollY / rowHeight) finds the first intersecting row; the last one comes from the viewport's bottom edge — ceil then -1 because a row that touches the bottom edge pixel is still visible. Off-by-one here is exactly the class of bug that shows as a blank strip at the bottom of a fast scroll.
  • The clamps aren't decoration. Overscroll (iOS bounce) sends negative scrollY; scroll-to-end sends a bottom edge past the content. Both would index outside the data. Handling the physical world's inputs — bounce, momentum — is the difference between the whiteboard version and the shipping version.
  • The empty list is its own case. With zero items there is no valid index, so clamping to itemCount - 1 (= -1) is nonsense. Return an empty range{ first: 0, last: -1 }, where last < first means "iterate nothing" — before the arithmetic runs. A list that renders and then empties (filter, search) hits this every time.
  • Real FlatList renders windowSize viewports around this range as a buffer; the math is identical with padding added to both ends.

Red flag: claiming getItemLayout speeds up rendering. It doesn't render anything faster — it removes measurement as a dependency for positioning, which unlocks scrollToIndex and eliminates layout passes. Precision about what it skips is the seniority signal.

Say it: "Fixed row height turns layout into arithmetic: offset is height times index, the visible window is floor and ceil over the scroll edges, clamped for bounce — that's all a virtualized list is doing."