Part 3 of Pretext: The 15 kb Library That Bypasses Your Browser’s Most Expensive Operation
The Demo
Hit Simulate Stream to watch AI messages arrive token by token. Each bubble expands smoothly as text fills in — no jank, no reflow, no layout thrashing. Then switch to 🐢 DOM mode and run it again to see the difference in the fps counter.
Loading pretext…
⚡ pretext — arithmetic layout, zero DOM reads per frame
What’s Happening
In ⚡ pretext mode, every frame does this:
- Get the current streamed text slice
- Call
prepare(text, font)— canvas measurement, runs once when text changes - Call
layout(prepared, bubbleWidth, lineHeight)— pure arithmetic, returns height - Set
style.heighton the bubble
No DOM reads after step 2. The browser’s layout tree is never dirtied by the measurement itself.
In 🐢 DOM mode, every frame does this instead:
- Get the current streamed text slice
- Create a hidden
<div>, set its text and styles, append todocument.body - Call
getBoundingClientRect()— forces synchronous layout reflow - Remove the hidden element
- Set
style.heighton the bubble
The forced reflow in step 3 blocks the main thread. With multiple messages streaming simultaneously, the reflows compound.
The Core Pattern
import { prepare, layout } from '@chenglou/pretext'
function measureBubbleHeight(text: string, maxWidth: number): number { const prepared = prepare(text, '14px ui-sans-serif, system-ui, sans-serif') const { height } = layout(prepared, maxWidth, 22) return height + 24 // add padding}
// Inside your streaming update handler:const height = measureBubbleHeight(currentText, bubbleWidth)element.style.height = `${height}px`In production you’d cache the PreparedText handle across streaming ticks — only call prepare() when the full text changes significantly, not on every token. The layout() call is the cheap one; it’s the one you call per frame.
Handling Multiple Simultaneous Streams
Where the gains compound is when multiple messages are streaming at once. Pretext makes each one independent:
// All of these run in the same animation frame with no reflowfor (const message of streamingMessages) { const { height } = layout(message.prepared, containerWidth, LINE_HEIGHT) message.element.style.height = `${height}px`}With DOM measurement, each element access would force a reflow that potentially invalidates the measurements you just computed for the previous elements. With pretext, all measurements are independent arithmetic operations on cached data.
The pre-wrap Case
Textarea-like inputs need white-space: pre-wrap to respect hard newlines and tabs:
import { prepare, layout } from '@chenglou/pretext'
const prepared = prepare(textareaValue, '14px ui-monospace', { whiteSpace: 'pre-wrap' })const { height } = layout(prepared, textareaWidth, 20)This is useful for auto-growing textareas — one of the most common patterns that traditionally required DOM measurement loops.