How would you handle dynamic row heights
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.
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
- Estimate first. Give every unmeasured row an estimated height. The total scroll height = sum of estimates. The scrollbar is approximate but usable immediately.
- Measure on render. When a row mounts, measure its real height (
ref+getBoundingClientRect, or aResizeObserver) and store it in a height cache keyed by row id/index. - 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.
- 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
scrollTopso the content doesn't visibly jump. - Watch for changes after mount — images loading, content expanding, font swaps. A
ResizeObserverper 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.