[labs/virtualizer] Reconsider viewport semantics — current logic conflates visibility with available layout space
- Dominant language
- TypeScript
- Stars
- 21.8k
- Forks
- 1.1k
- Avg merge
- 18h 25m
- Merged PRs (30d)
- 2
Description
## Problem
A core premise of virtualization is that *the virtualized view should be laid out and rendered just as an unvirtualized equivalent would be* — virtualization is purely an optimization that limits which items are rendered along the scroll axis, not a transformation of the layout itself.
The current `Virtualizer._updateView()` computes a single `viewportSize` from the geometric intersection of the host element and all of its clipping ancestors, and uses this same value for **both** layout-sizing decisions and range calculation, on **both** axes. This conflates two concerns that should be independent.
## Two distinct concerns
1. **Available layout space** — what the layout uses for sizing decisions (column counts, item dimensions, etc.). This should always reflect the host's configured layout dimensions on **both axes**, regardless of clipping ancestors. The layout's decisions should match what an unvirtualized equivalent would produce.
2. **Currently visible range** — what determines which items fall in the rendered range. This **should** be affected by clipping ancestors (e.g., a virtualizer fully off-screen should render nothing; one partially clipped along its scroll axis should render only the visible portion), but **only along the virtualization axis**. Clipping along the non-virtualization axis should never affect which items are rendered.
## Examples
**Cross-axis layout (currently broken):** A virtualizer with explicit `inline-size: 800px` using a grid layout, placed inside a parent of width `600px` with `overflow: auto`. The user has scrolled the parent so that only the leftmost 600px of the virtualizer is visible.
- *Unvirtualized equivalent*: the grid lays out across the full 800px (e.g., 4 columns at 200px each); the user scrolls the parent horizontally to reveal the rightmost column.
- *Current behavior*: `viewportSize.inlineSize` is 600px, so the grid lays out 3 columns. The configured 800px is effectively ignored.
**Scroll-axis visibility for a `scroller: true` virtualizer (status uncertain):** A `scroller: true` virtualizer placed inside a scrollable parent that scrolls it in and out of view.
Tracing through the current code: scroll listeners are attached to the parent (via `getClippingAncestors`), so the virtualizer does re-run its update cycle as the parent scrolls. The inset/viewport math then produces a reduced `viewportSize.blockSize` and a `viewportScroll.block` adjusted to account for the clipped portion, which has the *approximate* effect of rendering only the items that fall within the visible window. When the virtualizer is fully off-screen the math degenerates to a zero-size viewport, which likely results in no items rendered.
This *appears* to roughly implement visibility-driven range calculation, but it's unclear whether the behavior is deliberately designed or an emergent side-effect of the geometry math. There are no tests covering these configurations, the same logic also drives the (broken) cross-axis behavior in Example 1, and the off-screen case has not been verified to produce zero rendered items in practice. We should document the actual behavior, add coverage, and decide whether to formalize and refine it.
## Goal
Take inventory of the cases where current viewport calculation logic produces results that diverge from the unvirtualized-equivalent premise, and from the visibility-driven-range principle. Decide what the intended behavior should be, and reconcile the implementation accordingly.
A likely direction: distinguish between *available layout space* (per axis, never affected by ancestor clipping) and *currently visible range* (only along the virtualization axis, affected by ancestor clipping including for `scroller: true` virtualizers).
This issue is a placeholder for that work; concrete subtasks (audit, tests, design proposal, implementation) will follow.
Contributor guide
Research direction
Start with Virtualizer._updateView() and trace the clipping-ancestor handling through getClippingAncestors. Audit the cross-axis and scroller:true cases, then add tests for clipped, partially visible, and fully off-screen virtualizers. Done means the intended layout-space and visible-range behavior is documented, covered by tests, and reconciled in the implementation.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- frontend, web-dev
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 35/100