Virtual scroll
Ten thousand paragraphs, each a different length, arranged like an anthology you can scroll through. The browser only ever keeps a dozen of them in the DOM. The scroll is smooth because the height of every paragraph was decided before you arrived.
Crank the item count slider to watch mount time scale. Scroll the window; only the visible paragraphs (plus a screen of buffer on each side) are rendered. The height in the corner is the sum of every paragraph's pre-computed height — the inner spacer is set to exactly that tall.
Mechanism
At mount, we call prepare(text, font) on every item once, then layout(handle, width, lineHeight) at the viewport's inner width. We store two parallel arrays: heights[i] for each item and offsets[i] for the cumulative sum. The last offset is the total content height, which becomes the inner spacer's fixed height. The viewport itself is a plain overflow-auto box; the browser's native scrollbar does the navigation.
On every scroll event — and this is the whole point — we do no measurement. We binary-search offsets[] for the first index whose offset is ≥ scrollTop, then walk forward until we cover the viewport plus one screen of buffer on each side. Items outside that window are not in the DOM at all. Items inside are placed with position: absolute; top: offsets[i].
Because the heights came from Pretext and the CSS renders at the same font and line-height, every paragraph lands exactly where the offsets said it would. No sibling jitter, no post-mount correction, no wobble on fast scroll.
Application
Every list that currently feels slow at the high end is blocked by the same missing primitive: I do not know how tall this row is until I render it. With that gone:
- Chat histories with tens of thousands of messages, each variable height, scrolling as smoothly as a feed of fixed-height cards.
- Documents of arbitrary length where the scrollbar thumb size is honest — it reflects actual content, not a per-row estimate.
- Endless feeds where jump-to-index is constant-time:
scrollTop = offsets[i]. - Per-row anchoring and deep-linking that survives viewport resize: recompute heights once on width change, then the offsets are truth again.
"Our life is frittered away by detail. An honest man has hardly need to count more than his ten fingers, or in extreme cases he may add his ten toes, and lump the rest."
Henry David Thoreau, Walden (1854)Direct Claude
heights[] and offsets[]
"no measurement during scroll"
→
scroll handler does binary search + arithmetic only; no DOM reads
"scrollbar should reflect real content length"
→
inner spacer height = sum of pretext-reported heights
"jump to message N should be instant"
→
scrollTop = offsets[n] — no layout needed
import { prepare, layout } from '@chenglou/pretext';
// Mount: one prepare + one layout per item. Build offsets[] cumulatively.
const FONT = "400 18px 'EB Garamond', serif";
const LINE_H = 18 * 1.55;
const W = viewport.clientWidth - 44; // minus padding
const heights = new Float32Array(items.length);
const offsets = new Float32Array(items.length + 1); // +1 for total
let total = 0;
for (let i = 0; i < items.length; i++) {
const handle = prepare(items[i].text, FONT);
heights[i] = layout(handle, W, LINE_H).height;
offsets[i] = total;
total += heights[i] + ITEM_PADDING_Y;
}
offsets[items.length] = total;
spacer.style.height = total + 'px';
// Scroll: binary search + a windowed walk. No measurement.
function onScroll() {
const top = viewport.scrollTop - BUFFER;
const bottom = top + viewport.clientHeight + 2 * BUFFER;
const first = binarySearchLE(offsets, top);
const last = binarySearchLE(offsets, bottom);
renderRange(first, last); // create/reuse DOM, position each at offsets[i]
}