Concepts — A Virtualized List Engine

Phase 4 · Spec area §37. Prerequisites: bi-08, bi-09, bi-10.

1. Why this matters

Virtualization is the clearest case in application code where browser internals determine the architecture. You cannot design it well without knowing what DOM size costs (bi-04), what layout costs (bi-08), and why scrolling is compositor-driven (bi-10).

It is also where "measure, don't guess" is unavoidable: dynamic row heights require measurement, and measurement forces layout.

2. Build order

Render 100,000 logical rows with a small DOM window.

  1. Fixed heights: viewport → visible index range → render window + overscan.
  2. Absolute positioning or transform offset for the window. Choose deliberately and justify it from bi-09.
  3. Dynamic heights: measure rendered rows, cache measurements, correct the estimate.
  4. Scroll anchoring: keep the visually-anchored row stable when an above-viewport measurement changes.
  5. Fast scroll: what to show when you outrun measurement.
  6. Accessibility: what a screen reader sees when only 20 of 100,000 rows exist.

3. Failure Lab

  1. Measurement thrash. Measure every row every frame. Watch forced synchronous layout (bi-08).
  2. Anchor drift. Skip scroll anchoring; watch content jump as heights resolve.
  3. Overscan zero. Blank rows on fast scroll — the app-level analogue of checkerboarding (bi-10).
  4. DOM growth leak. Recycle rows incorrectly so the DOM grows; find it in a heap snapshot.
  5. Scroll handler on the main thread. Update the window in a scroll handler; measure the one-frame lag. Compare an IntersectionObserver/sentinel approach.

3.5 Deep dive: the measurement problem

Fixed heights make position arithmetic O(1): offset = index * rowHeight. Dynamic heights destroy that, and the resulting problems are all one problem — you cannot know the total height without measuring every row, and you cannot measure a row without rendering it.

The standard resolution is estimate-then-correct:

  1. Assume an estimated height for unmeasured rows.
  2. Measure rows as they render; cache by item id (not by index — indices shift).
  3. Maintain a prefix-sum structure so "what is the offset of row N" stays fast.
  4. When a measurement differs from the estimate, correct the scroll offset if the changed row is above the viewport, or the content under the user's finger jumps.

Step 4 is the one that separates a working virtualizer from a demo. It is scroll anchoring (bi-08), implemented by hand, and it is why the browser's built-in version exists.

A prefix-sum / Fenwick tree over row heights gives O(log n) offset lookups and O(log n) updates, versus O(n) for a naive running total recomputed on every measurement. At 100,000 rows that is the difference between smooth and unusable.


3.6 Deep dive: why measurement is expensive, precisely

Measuring means reading geometry, which forces style and layout to be current (bi-08). Doing it per row, per frame, interleaved with writes, is the forced-synchronous-layout bug at scale.

The mitigations, in order of preference:

  1. ResizeObserver — delivered inside the rendering steps, after layout, so it does not force a synchronous flush (bi-11).
  2. Batch reads, then writes — measure everything, then apply all offsets.
  3. IntersectionObserver for visibility rather than repeated getBoundingClientRect.
  4. content-visibility: auto with contain-intrinsic-size — let the engine skip layout for offscreen content and supply an estimate, which is the platform doing your job.

Option 4 is worth serious consideration before writing a virtualizer at all: it keeps every row in the DOM (so find-in-page, accessibility, and anchor links work) while skipping their layout and paint. It does not reduce DOM size, so it is not a substitute at 100,000 rows — but at 2,000 it frequently is.


3.7 Deep dive: what virtualization breaks

This is the section most tutorials omit, and it is where the real engineering judgement is.

BreaksWhyMitigation
Find-in-page (Ctrl+F)offscreen rows are not in the DOMnone reliable — a genuine loss
Screen reader navigationAT sees 20 of 100,000 rowsaria-setsize / aria-posinset, and a real grid role
Anchor links / scrollIntoViewtarget may not existroute through your own index → scroll
Tab orderfocusable elements appear and disappearmanage focus on recycle; never leave focus on a removed node
Text selection across rowsselection breaks at the window edgerarely fixable
Printonly the window printsrender an unvirtualized print view
Browser scroll restorationheight changes as rows measuremanual restoration (fw-08)

The accessibility row deserves emphasis. aria-setsize and aria-posinset let you tell assistive technology "this is item 4,207 of 100,000" even though only 20 exist — an author-supplied promise substituting for information the runtime cannot observe, which is the fifth instance of that pattern in this track.

The Principal-level framing: virtualization is not a free optimisation. It trades a set of platform behaviours you got for nothing in exchange for render performance. Whether that trade is right depends on the product — a data grid for analysts, yes; a documentation page, almost certainly not. The engineer who reaches for virtualization by default has not priced the right-hand side.

4. Trade-offs

Fixed vs dynamic heights. Fixed makes position arithmetic O(1) and is often a lie about the data.

Bigger overscan. Fewer blanks, more DOM, more layout.

transform vs top. bi-09's property-tree argument, applied.

Virtualization vs content-visibility. The platform now offers a partial answer. When does the built-in beat the library?

5. Principal Engineer Review

  1. Explain why virtualization helps, in terms of which pipeline stages it removes work from.
  2. Dynamic measurement forces layout. Design the scheme that minimises it; state what you give up.
  3. Argue that content-visibility: auto makes list virtualization obsolete. Then defeat it.
  4. What does virtualization do to accessibility and find-in-page, and what is your mitigation?
  5. A 100k-row grid must support sort, filter, and inline edit. Where does virtualization stop being the hard part?