Virtualization
A two-phase layout for large lists: items inside the visible range (± an overscan buffer) get measured and positioned normally; items outside it skip real DOM measurement entirely and are packed from an estimated size instead, so setItems()/relayout() stay cheap regardless of total list size.
Turning it on
MasonryOptions.virtualize / scrollContainer / estimateSize
virtualize
boolean | { overscan?: number } · default: false
false (or omitted) — every item is always measured for real, regardless of list size. true turns virtualization on with the default overscan (600). An object form lets you set overscan explicitly.
scrollContainer
HTMLElement | 'self' | 'window' · default: 'window' at direction: 'vertical', 'self' at 'horizontal'
Which element's scroll position/viewport size the engine tracks to decide the visible range. 'self' — the engine's own container scrolls (the typical case at 'horizontal', where the container is already a fixed-size scroll viewport). 'window' — the page scrolls (the typical case at 'vertical'). An explicit element — a custom scroll ancestor outside the container (e.g. a scrollable panel the grid sits inside, that isn't the container itself). Ignored when virtualize is off.
estimateSize
(item: MasonryItemDescriptor) => number · optional
Fallback main-axis size estimate (px), used only when a given item has neither estimatedSize nor aspectRatio set. Ignored when virtualize is off.
Example:
const engine = createMasonryEngine(container, {
virtualize: { overscan: 800 }, // track a wider buffer than the 600px default
scrollContainer: 'window', // the page scrolls, not the container itself
estimateSize: (item) => item.estimatedHeight ?? 200, // fallback for items with neither estimatedSize nor aspectRatio
})Off-Screen Item Size Resolution
Without virtualize, an item with no real measurement yet (and no aspectRatio) is simply left out of the layout until there's a way to measure it. One such case is an item whose element is (or contains) an <img> that hasn't finished loading — the engine doesn't trust its current getBoundingClientRect() at all, since a not-yet-loaded <img> with no known intrinsic size and no CSS aspect-ratio falls back to the browser's own 150px default-replaced-element height, which reads as a real measurement without being one. Instead of ResizeObserver, the engine attaches its own load/error listener to that image and relayouts once it fires. Practical effect: an image gallery with no aspectRatio on any item still lays out correctly, undistorted — cards just appear one at a time as each image finishes loading, instead of all settling immediately from an estimate. With virtualize on, the engine instead resolves a main-axis size through this chain, in order:
- A real measurement, if the item currently has a mounted element (i.e. it's inside the visible range right now) — except an item with an unloaded
<img>inside it (see above), which never counts as "measured" even while mounted, and falls through to the next step instead. - The last known real measurement, if the item was measured before but has since scrolled out of view and its element unmounted — persisted across relayouts so a virtualized item stays self-corrected instead of reverting to a raw estimate every time it leaves the measured window.
MasonryItemDescriptor.estimatedSize, if set — takes priority overaspectRatiosince it's an explicit per-item override.MasonryItemDescriptor.aspectRatio, if set — the item's resolved cross size divided (vertical) or multiplied (horizontal) by the ratio.MasonryOptions.estimateSize(item), if provided.- A guessed fallback: the average of whatever's actually been measured so far, or the item's own cross size (a "roughly square" guess) if nothing has been measured yet at all. This path also logs one
console.warnthe first time it's hit, since silently guessing on a virtualized list is easy to miss until the layout visibly looks wrong — setestimatedSize,aspectRatio, orestimateSizeto avoid it.
Items in the Visible Range
engine.getVisibleIds() and the layout event's visibleIds field both return the same set — every id that should have a real DOM element right now. With virtualize off, that's every packed item. With it on, it's only the ones inside the visible range (± overscan); the <MasonryGrid> component/render-prop only mounts a wrapper (and calls your #item slot / children render function) for ids in this set — everything else exists purely as an estimate inside the engine, not in the DOM at all.
engine.on('layout', ({ visibleIds }) => {
console.log(`${visibleIds.length} items currently have a real DOM element`)
})