01 — The Position Lookup Problem
A virtual scroller only mounts the items currently visible in the viewport. To know which items are visible at a given scroll position, it must map scrollTop to an item index. That mapping has two very different costs depending on whether items share the same height.
The panel on the right shows both worlds side by side. Fixed-height items are uniform — one division finds any item. Variable-height items vary from 28px to 96px, and finding the right one requires searching through cumulative offsets.
Every virtual list library faces this exact tradeoff. The next five steps walk through each side.
02 — Fixed Height: Direct Math
When every item is exactly 40px tall, the position of item #200 is 200 × 40 = 8000px. No data structure needed — one multiplication gives you the exact pixel offset. Scrolling to any position is equally trivial: floor(scrollTop / 40) gives you the first visible item.
This is why every virtual list library defaults to fixed-height mode. Libraries like react-window require you to pass an itemSize number, and they use this direct math internally.
Drag the scroll position slider on the right to watch the formula compute instantly. The highlighted item tracks the result of floor(scrollTop / itemHeight) — a single O(1) operation regardless of list size.
03 — Variable Height: Prefix Sums
When items have different heights, the position of item #200 depends on the heights of items #0 through #199. You need a prefix-sum array — a cumulative offset table where offsets[i] equals the sum of all heights before item i.
Building this array is O(n) and done once. Each entry is just offsets[i] = offsets[i-1] + heights[i-1]. The right panel shows the height distribution as a bar chart and the resulting offset array below it. Hover over bars to see how each height contributes to the next offset.
04 — Binary Search on Offsets
Looking up a scroll position requires finding which offset range contains the target pixel — a classic binary search on a sorted array. The offset array is inherently sorted (heights are positive), so binary search works directly.
Each step halves the search range. For 50 items, binary search takes at most 6 steps. For 10,000 items, at most 14 steps. The right panel lets you step through the search one comparison at a time — watch the range narrow from [0..49] to the exact item.
The key insight: binary search on a prefix-sum array gives O(log n) position lookup. That's the fundamental data structure behind every variable-height virtual scroller.
05 — The Estimation Problem
There's a catch: you often don't know item heights until they render. Text wraps differently based on container width, images load asynchronously, and content is dynamic. Libraries like @tanstack/virtual solve this with a two-phase approach:
- Estimate first — use a default height (say 50px) for all items to create the initial offset array and scrollbar
- Measure after render — use ResizeObserver to capture the actual rendered height of each item
- Correct the offsets — update the prefix-sum array with measured heights
The right panel shows the gap between estimated heights (uniform 50px bars) and actual measured heights (varying 28-96px). The delta between estimated and actual totals is the scroll correction the library must apply — this is why variable-height virtual lists sometimes “jump” slightly.
06 — The ResizeObserver Correction Cycle
The ResizeObserver API is the measurement mechanism. You create one observer, attach it to each rendered item, and it fires a callback whenever an item's size changes. The callback updates the prefix-sum array, which triggers a re-render with corrected positions.
Click through the four phases on the right to see the cycle: Estimate Render Measure Correct. This cycle runs continuously as the user scrolls — new items enter the viewport, get measured, and their heights replace the estimates.
This estimation-correction cycle is why @tanstack/virtual exposes a measureElement ref callback. You attach it to each item's DOM node, and the library handles the rest — measuring via ResizeObserver, updating offsets, and adjusting scroll position.
The Resize Observer stop in the Web APIs section covers the measurement mechanism in detail. This stop focused on why measurement is needed: because variable heights can't be known until render, and the position lookup that makes virtual scrolling work depends on accurate heights.