lit / lit/lit

[labs/virtualizer] Reconsider viewport semantics — current logic conflates visibility with available layout space

Open
#5,291 0 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.