chenglou/pretext measures & lays out multiline text using the canvas as ground truth instead of the DOM, so you never trigger a reflow just to ask "how tall is this paragraph at 320px?"
To measure text the browser-native way, you put it in an element and read offsetHeight / getBoundingClientRect(). But those reads force a synchronous reflow: the browser must lay out the page right then to answer. Do it once and it's fine; do it in a loop (virtualized lists, measuring hundreds of items, re-measuring on every resize frame) and a reflow becomes one of the costliest things on the main thread. pretext splits the work in two:
prepare(text, font): the one-time cost: normalize whitespace, segment (graphemes, since it handles CJK, Arabic, emoji, ZWJ), then measure each segment with canvas measureText. Cache it. Never re-run for the same text+font.layout(prepared, width, lineHeight): the cheap part: pure arithmetic over the cached widths. Re-run it free on every resize tick. No DOM, no reflow.This runs the real pretext (v0.0.7, vendored locally; see the closing note on keeping its bytes off every other page). Left: the browser laying the text out and reporting offsetHeight (a reflow). Right: pretext's layout() over a one-time prepare(), same line count, zero DOM. Drag the width:
The thing a width-only measure can't do: text whose available width changes per line. pretext's layoutNextLine(prepared,
cursor, width) lays out one line at a time, so you feed it a narrow width while the line sits beside the floated box and the full width once the text clears it. The result is real shaped wrapping, drawn to a canvas, no DOM. Drag the box bigger and watch the text reflow around it:
Here's the gap a 30-line re-creation falls into. A naïve split(/\s+/) assumes spaces separate words, so CJK (which has none) becomes one giant token that overflows, and scripts it can't segment get mangled. pretext segments by grapheme via Intl.Segmenter and breaks the same string correctly:
Same paragraph, re-laid-out at many widths (the resize / virtualization pattern). The DOM path forces a reflow each time; the canvas path just crunches arithmetic over the prepared widths (prepared once, outside the loop). Run it:
aspect-ratio guesses. Niche here, but real.system-ui is explicitly unsafe for accuracy on macOS.white-space: normal/pre-wrap, a few break modes, pixel letter-spacing; no font-feature-settings / optical sizing.Intl.Segmenter + Canvas 2D (both broadly shipped now, incl. Safari).On the dependency: this page runs the real github.com/chenglou/pretext, bundled to a single 12 KB-brotli ES module and vendored same-origin at /garage/pretext.lib.js. Because it's served from 'self', the existing CSP needs no change, so pretext's bytes load only when you open this page and never touch the homepage or any other page. The only other demo here that's mine is the naïve split(/\\s+/) wrapper in section 4, kept deliberately bad to show the gap.