Pretext Lab
05Reusable handle
Arc 2 · Basic Measurement

Reusable handle

One analyze, many fits. prepare() is the expensive step — it segments, measures, caches. layout() is cheap arithmetic. Once you hold a handle, you can ask for any width and get the exact height, as many times as you like, without repeating the measurement.

Below, four panes show the same paragraph at mobile, tablet, desktop, and wide breakpoints. One prepare() call at boot produced the handle. Four layout() calls filled the panes. Nudge the font size — all four update in lock-step.

mobile · 320
I contain multitudes.
height px · lines
tablet · 480
I contain multitudes.
height px · lines
desktop · 720
I contain multitudes.
height px · lines
wide · 1040
I contain multitudes.
height px · lines
1 prepare call · 4 layout calls · last pass μs
16

Mechanism

A Pretext handle is an opaque cache of "everything the text and font say that doesn't depend on width." Segments. Glue rules. Canvas-measured widths. Bidi levels where relevant. None of that changes when you ask about a new width, so Pretext doesn't recompute it.

layout(handle, width, lineHeight) walks those cached segments and applies word-wrap rules in arithmetic. On the library's benchmark snapshot, prepare() averages about 19ms for a 500-text batch; layout() for that same batch averages about 0.09ms. The ratio is roughly 200:1. A handle pays for itself on the second call and compounds from there.

Practical consequence: the right unit of caching is the handle, not the rendered height. Store handles keyed by (text, font); never cache by width. Widths are cheap to answer about.

Application

"Cached across breakpoints" stops being aspirational and becomes the default:

The heuristic: if two regions display the same text in the same font, they share one handle. The only thing that differs between them is the argument to layout().

"Do I contradict myself? Very well then I contradict myself, (I am large, I contain multitudes.)"

Walt Whitman, Song of Myself, from Leaves of Grass (1855)

Direct Claude

"one content piece, every viewport" one prepare(), one layout() per column width you need "cache across breakpoints" keep the handle keyed by (text, font); never key by width "three widths, side by side" three layout() calls on the same handle — no extra measurement "pick the nicest wrap" sweep widths via layout(); take the lineCount you like
Next arc: whitespace and breaks — when the text isn't prose, the rules shift.
one handle, four layouts
import { prepare, layout } from '@chenglou/pretext';

const FONT = "400 16px 'EB Garamond', serif";
const LINE_HEIGHT = 16 * 1.5;
const BREAKPOINTS = [320, 480, 720, 1040];

// Prepare once. Analyze cost paid.
const handle = prepare(TEXT, FONT);

// Lay out everywhere. Pure arithmetic per call.
for (const width of BREAKPOINTS) {
  const { height, lineCount } = layout(handle, width, LINE_HEIGHT);
  // ...place each pane using its own height.
}