Back to React
React
medium
mid

How would you handle dynamic row heights in a virtualized list?

Measure rows as they render, cache their heights, and maintain a running offset index (prefix sums) to map scroll position to row index. Use estimated heights for unmeasured rows and a ResizeObserver to catch changes after mount.

6 min read·~15 min to think through

Fixed-height virtualization is easy — index = scrollTop / rowHeight. Dynamic heights break that math, because you can't know a row's offset without knowing every prior row's height. This is the hard part of virtual scrolling.

The core problem

To render the visible window you need: given scrollTop, which rows are visible? With variable heights you need each row's cumulative offset, which depends on all earlier rows' actual heights — many of which haven't rendered yet.

Strategy

  1. Estimate first. Give every unmeasured row an estimated height. The total scroll height = sum of estimates. The scrollbar is approximate but usable immediately.
  2. Measure on render. When a row mounts, measure its real height (ref + getBoundingClientRect, or a ResizeObserver) and store it in a height cache keyed by row id/index.
  3. Maintain a position index. Keep a prefix-sum array of offsets so "offset → index" and "index → offset" are fast lookups (binary search over the prefix sums = O(log n)). Update it as measurements come in.
  4. Reconcile. When a measured height differs from the estimate, the total height and subsequent offsets shift. Update the spacer height; if the changed row is above the viewport, compensate scrollTop so the content doesn't visibly jump.
  5. Watch for changes after mount — images loading, content expanding, font swaps. A ResizeObserver per rendered row catches these and re-measures.

Why it's tricky

  • Scroll anchoring — re-measuring a row above the viewport must not make the content jump under the user.
  • Estimate accuracy — bad estimates make the scrollbar lurch as the user scrolls into real content.
  • Measurement cost — measuring forces layout; do it in batches, not per-frame.
  • scroll-to-index for an unmeasured row is approximate until things settle.

Practical advice

This is exactly why you use a library — @tanstack/react-virtual and react-virtuoso implement measurement caches, ResizeObserver integration, and scroll anchoring properly. Building it yourself means owning all of the above.

Follow-up questions

  • Why do you need a prefix-sum / position index?
  • How do you prevent content from jumping when a row above the viewport is re-measured?
  • How do you handle a row whose height changes after mount (image load)?
  • How does scroll-to-index work when the target row hasn't been measured?

Common mistakes

  • Assuming a single rowHeight and breaking on variable content.
  • Not compensating scrollTop when re-measuring rows above the viewport.
  • Measuring every row every frame, forcing constant layout.
  • Poor height estimates causing the scrollbar to lurch.

Performance considerations

  • Measurement forces synchronous layout — batch it and cache aggressively. Binary search over prefix sums keeps offset lookups O(log n). The estimate quality directly drives perceived scroll smoothness.

Edge cases

  • Images or async content changing height after first render.
  • Font swap (FOUT) shifting text height.
  • Window resize re-flowing every row.
  • Scroll-to-index into the far, unmeasured part of the list.

Real-world examples

  • Chat apps where messages vary from one line to image attachments.
  • Feeds with mixed media cards of unpredictable height.

Senior engineer discussion

Seniors identify the root difficulty — an offset depends on all prior heights — and reach for the estimate → measure → cache → prefix-sum loop, plus scroll anchoring to avoid jumps. They're explicit that this is the strongest argument for using a library rather than hand-rolling virtualization.

Related questions