React Window Virtualization for Dynamic Lists with Media
Fix layout shakes in react window and react virtualized for dynamic React lists with frequent updates, images, buttons. Code examples, react-virtuoso alternative, patterns for smooth performance.
How to implement virtualization for a dynamic React list with frequent updates, media content, and buttons?
I have a list of data from the backend that updates frequently (items are added, removed, or modified). Each item can contain text, media (e.g., images), and interactive buttons.
Challenges with react-virtualized:
- Layout shakes horribly when data changes.
- CellMeasurer often fails to account for button heights accurately.
What I’m seeking:
- Alternative libraries that handle dynamic updates, complex item content (media + buttons), and smooth performance better than react-virtualized.
- Or advanced react-virtualized examples/code snippets that address these issues with real-world dynamic data scenarios.
Most online examples are too simplistic and don’t cover frequent updates or mixed content types.
Use a library that handles variable heights and item measurement (react-virtuoso or react-window with ResizeObserver + stable itemKey) and apply these patterns: reserve layout space for media, measure and debounce size changes, avoid index-based keys, and reset measurement caches selectively. If you must stay on react-virtualized, combine CellMeasurerCache clearing + list.recomputeRowHeights(index) triggered after media/button mount/resize and use stable keys to stop DOM-reuse focus jumps.
Contents
- Problem: why react-virtualized shakes with dynamic content
- React Window and alternatives for dynamic lists
- React virtualization patterns that stop layout-shakes
- Code: react-window VariableSizeList + ResizeObserver pattern
- Code: react-virtuoso example (handles variable heights automatically)
- Advanced react-virtualized fixes (CellMeasurer tips)
- Performance tuning & checklist for media + buttons
- When to pick which library (tradeoffs)
- Sources
- Conclusion
Problem: why react-virtualized shakes with frequent updates and mixed media
You’re seeing layout shakes because virtualization relies on stable item sizes or accurate measurements; when items change height (images load, text wraps, buttons render, or items are inserted/removed) the cached sizes become stale and the virtual scroller reuses DOM nodes (index-based recycling), which moves content around. CellMeasurer can help, but it often measures too early (before fonts/CSS/buttons finish rendering) or the cache isn’t invalidated correctly, so measured heights are wrong and rows jump.
Two common root causes:
- DOM/node reuse with index-as-key: the list rebinds a different item into an existing DOM node, causing focus and button state to jump.
- Late-changing heights (images, async content, buttons styled by fonts): measurements taken on mount miss later size changes unless you re-measure.
(For search-intent context, Yandex Wordstat shows “react window” and “react virtualized” are high-volume queries — the choice of wording matters when people search for solutions.) See Yandex Wordstat for query volumes: Yandex Wordstat — topRequests: react window.
React Window and alternatives for dynamic lists
Options that handle dynamic updates better than an out-of-the-box react-virtualized setup:
- react-virtuoso — excellent at automatic variable-height measurement and tolerant of frequent inserts/removals; it minimizes layout flashes for mixed content.
- react-window (the lighter successor to react-virtualized) — use VariableSizeList + manual measurement (ResizeObserver) and itemKey to avoid DOM-reuse problems.
- RecyclerListView — performant for extreme throughput (often used in RN and heavy web use-cases), but more complex to integrate.
- Stay with react-virtualized only if you need some specific components; otherwise prefer react-window or react-virtuoso and apply the patterns below.
Note: search queries often mix “react window” vs “react windows” (ambiguous); if you’re optimizing help pages, disambiguate the library name vs OS queries and mention both forms. See the related Wordstat entries for awareness: Yandex Wordstat — topRequests: react virtualized.
React virtualization patterns that fix layout shakes
These patterns are applicable across libraries (react-window, react-virtuoso, react-virtualized).
- Stable keys — never use index as key. With react-window, set itemKey so nodes aren’t recycled incorrectly:
- itemKey=
-
Reserve space for media — give images an intrinsic aspect ratio or an explicit placeholder height so the initial measurement is close to final (no shift). Use CSS aspect-ratio or width+height attributes.
-
Measure changes reliably — use ResizeObserver (or onLoad for images) to detect when an item’s height actually changes, then tell the virtualizer to re-measure that item (resetAfterIndex or recomputeRowHeights).
-
Debounce/aggregate measurement updates — image loads or many small updates can fire many reflows. Debounce calls to reset/recompute to batch re-layouts.
-
Use overscan — a small positive overscanCount reduces visible white gaps during fast scroll.
-
Preserve focus & interactive state — save focused item id before updates, use stable keys (itemKey), and restore focus after render if needed.
-
Memoize item renderers — avoid re-rendering every item on each data change. Use React.memo and pass stable props.
-
Use startTransition for non-urgent updates — mark frequent incoming data changes as low priority to keep scrolling smooth.
-
Prefer libraries that auto-measure variable heights for you (react-virtuoso) if you want less plumbing.
Code: react-window VariableSizeList + ResizeObserver pattern
This pattern uses VariableSizeList, an item-size map, ResizeObserver inside each item, and a debounced resetAfterIndex. It addresses frequent inserts/removes, images, and buttons.
// Example (conceptual)
import React, { useRef, useEffect, useCallback } from 'react';
import { VariableSizeList as List } from 'react-window';
function useDebounced(fn, wait = 50) {
const t = useRef(null);
return (...args) => {
clearTimeout(t.current);
t.current = setTimeout(() => fn(...args), wait);
};
}
export default function VirtualList({ items, height, width, estimatedItemSize = 120 }) {
const listRef = useRef();
const sizeMap = useRef(new Map()); // key -> measured height
const getSize = index => {
const id = items[index].id;
return sizeMap.current.get(id) || estimatedItemSize;
};
const debouncedReset = useDebounced((index) => {
if (listRef.current) listRef.current.resetAfterIndex(index, false);
}, 50);
const Row = ({ index, style }) => {
const item = items[index];
const containerRef = useRef();
useEffect(() => {
if (!containerRef.current) return;
const ro = new ResizeObserver(entries => {
for (const entry of entries) {
const h = Math.round(entry.contentRect.height);
const prev = sizeMap.current.get(item.id);
if (prev !== h) {
sizeMap.current.set(item.id, h);
debouncedReset(index); // schedule recalculation once per burst
}
}
});
ro.observe(containerRef.current);
return () => ro.disconnect();
}, [item.id, index]);
return (
<div style={style}>
<div ref={containerRef}>
<img
src={item.image}
alt=""
style={{ width: '100%', height: 'auto' }}
onLoad={() => debouncedReset(index)} // ensure measurement after load
/>
<div>{item.text}</div>
<button onClick={() => console.log('clicked', item.id)}>Action</button>
</div>
</div>
);
};
return (
<List
ref={listRef}
height={height}
width={width}
itemCount={items.length}
itemSize={getSize}
overscanCount={4}
itemKey={index => items[index].id} // crucial: stable DOM mapping
>
{Row}
</List>
);
}
Notes:
- itemKey avoids DOM reuse problems that cause focus/button issues.
- ResizeObserver plus debounced resetAfterIndex prevents repeated full recalculations.
- Set an estimatedItemSize close to average item height to reduce initial jump.
- Use
onLoadon images to trigger measurement after the image finishes.
Code: react-virtuoso example (handles variable heights automatically)
If you want minimal plumbing, react-virtuoso measures native item heights and is robust for frequent changes:
import { Virtuoso } from 'react-virtuoso';
export default function VirtuosoList({ items }) {
return (
<Virtuoso
data={items}
itemContent={(index, item) => (
<div data-item-id={item.id}>
<img src={item.image} style={{ width: '100%', height: 'auto' }} alt="" />
<div>{item.text}</div>
<button onClick={() => doAction(item.id)}>Action</button>
</div>
)}
increaseViewportBy={200} // similar to overscan; tune to your needs
computeItemKey={index => items[index].id}
/>
);
}
Why this helps:
- Virtuoso measures items when they mount and after size changes, so you don’t need manual ResizeObserver plumbing.
- It handles inserts/removals smoothly and preserves focus when keys are stable.
If you’re evaluating libraries, compare react-virtuoso vs react-window for your update rates and complexity.
Advanced react-virtualized fixes (CellMeasurer tips)
If you must stick with react-virtualized, apply these targeted fixes:
- Use a CellMeasurerCache with a reasonable defaultHeight and fixedWidth (if width is constant).
- When images or fonts finish loading inside a cell, call:
- cache.clear(rowIndex)
- listRef.current.recomputeRowHeights(rowIndex)
- Use rowRenderer that wraps each row in CellMeasurer so per-row measurement is on-demand.
- Avoid index-as-key. If your List/Grid uses index everywhere, switch to a mapping so DOM nodes represent item ids.
- If many items change height simultaneously, avoid clearing the entire cache every frame; instead mark affected indices and recompute them in batches (debounce).
- If you need to force a full recompute (rare), call cache.clearAll() then force an update—but this can briefly reflow the whole list.
Example sketch (conceptual):
// pseudo for react-virtualized
const cache = new CellMeasurerCache({ defaultHeight: 120, fixedWidth: true });
const listRef = useRef();
function onImageLoad(index) {
cache.clear(index);
listRef.current.recomputeRowHeights(index);
}
Performance tuning & checklist for media + buttons
Quick checklist to apply and measure:
- Keys & DOM
- [ ] Use stable ids via itemKey/computeItemKey/keys — never index.
- Measurement
- [ ] Estimate an average item height to reduce initial jump.
- [ ] Use ResizeObserver or image onLoad to trigger per-item re-measure.
- [ ] Debounce resets (50–150ms) to batch rapid changes.
- Scrolling & UX
- [ ] Set overscanCount / increaseViewportBy a bit to avoid blank frames.
- [ ] Use startTransition for heavy incoming updates so scrolling stays responsive.
- Rendering
- [ ] Memoize item components (React.memo) and avoid inline function props passing new references every render.
- [ ] Keep item DOM shallow and move heavy logic out of item renderers.
- Media
- [ ] Add width/height attributes or use CSS aspect-ratio to reserve space.
- [ ] Lazy-load images with IntersectionObserver when appropriate.
- Focus & state
- [ ] Save focused item id and restore after data updates if necessary.
- [ ] Avoid uncontrolled DOM state that virtualizer might recycle.
- Measure success
- [ ] Test with real update patterns and use Chrome Performance (FPS, main-thread times).
- [ ] Use representative device/browser (mobile is often worst-case).
When to pick which library (tradeoffs)
- Choose react-virtuoso if: you want minimal measurement plumbing, lots of variable-height items, frequent updates, and quick dev iteration.
- Choose react-window if: you want a small, fast core and are willing to add ResizeObserver + item measurement code.
- Choose react-virtualized if: you rely on specific components it provides and are prepared to manage CellMeasurer cache complexities.
- Choose RecyclerListView if: you need the utmost throughput for thousands of updates per second (more complex integration).
If you’re unsure, prototype a small slice of your app with react-virtuoso and with react-window + ResizeObserver and compare memory, smoothness, and development cost.
Sources
- Yandex Wordstat — topRequests: react window
- Yandex Wordstat — topRequests: react virtualized
- Yandex Wordstat — topRequests: react virtualization
- Yandex Wordstat — topRequests: virtualized list react
- Yandex Wordstat — topRequests: dynamic list react
Conclusion
For smooth react virtualization with frequent updates, media, and interactive buttons: prefer a library that measures variable heights automatically (react-virtuoso) or use react window with ResizeObserver + itemKey + debounced resetAfterIndex to avoid layout shakes. If you must keep react-virtualized, clear and recompute CellMeasurerCache per affected index after images/buttons mount and never use index-as-key. Start with those patterns and iterate while profiling; you’ll likely see dramatic reductions in layout jumps and focus/interaction bugs with either react-virtuoso or the react window pattern.