fw-10 — Analysis
Required invariants
- Measurements are cached by item identity, not by index. Indices shift; identities do not.
- A measurement correction above the viewport adjusts scroll offset, or content jumps under the user's finger.
- Reads are batched separately from writes, or measurement becomes forced synchronous layout.
- Total height is derivable in better than O(n) — a prefix-sum or Fenwick structure.
- Removed rows release focus and listeners, or recycling leaks.
The circularity at the centre
You cannot know total height without measuring every row; you cannot measure a row without rendering
it. The resolution is estimate → measure → correct, and step "correct" is what separates a working
virtualizer from a demo. It is scroll anchoring (bi-08) implemented by hand — which is precisely
why the browser ships its own.
At 100,000 rows, a naive running total recomputed per measurement is O(n) per update; a Fenwick tree is O(log n). That is the difference between smooth and unusable.
Why measurement is expensive
Measuring reads geometry, which forces style and layout to be current. Per row, per frame,
interleaved with writes, that is the forced-synchronous-layout bug at scale. Mitigations in
preference order: ResizeObserver (delivered inside the rendering steps, after layout),
batched read-then-write, IntersectionObserver for visibility, and content-visibility: auto
with contain-intrinsic-size.
What virtualization breaks
| Breaks | Mitigation |
|---|---|
| Find-in-page | none reliable — a genuine loss |
| Screen-reader navigation | aria-setsize / aria-posinset, a real grid role |
Anchor links, scrollIntoView | route through your own index |
| Tab order and focus | manage focus on recycle |
| Text selection across rows | rarely fixable |
| render an unvirtualized print view | |
| Browser scroll restoration | manual restoration |
aria-setsize is the pattern again: an author-supplied promise substituting for information the
runtime cannot observe.
The judgement
Virtualization trades platform behaviours you got for free against render performance. Right for a
data grid; almost certainly wrong for a documentation page. An engineer who reaches for it by
default has not priced the right-hand column. Before adopting it, check whether
content-visibility: auto alone is sufficient — at a couple of thousand rows it frequently is, and
it keeps every row in the DOM.