Pretext — A Pure JS Layout Library That Measures Text Height Without DOM
Get glyph widths from Canvas measureText(), then line calculation is pure arithmetic — zero layout reflow
The Problem: Layout Reflow
Traditional ways to measure text height in browsers:
const rect = element.getBoundingClientRect();
const height = element.offsetHeight;
Both force layout reflow — the browser must recalculate the entire layout tree to determine the element's size. Fine for one-off measurements, but measuring heights for 500-1,000 virtual scroll items means repeated reflows that kill frame rates.
Installation
npm install @chenglou/pretext
import { prepare, layout } from '@chenglou/pretext'
// For rich text (mixed inline styles):
import { ... } from '@chenglou/pretext/rich-inline'
Pretext's Approach: Canvas measureText()
Core Idea:
1. Use Canvas.measureText() to get exact glyph widths from the font engine
2. Cache the widths
3. Line-break calculation becomes pure arithmetic — add glyph widths until exceeding container width, then break
4. Zero DOM access, zero reflow
const prepared = prepare('AGI 春天到了. بدأت الرحلة 🚀', '16px Inter')
const { height, lineCount } = layout(prepared, textWidth, 20)
prepare() measures glyph widths via Canvas (heavy, ~19ms for 500 texts). layout() computes height from cached widths (light, ~0.09ms for 500 texts). Prepare once, layout as many times as needed.
Performance
| Stage | Time (500 texts) | Description |
|---|---|---|
prepare() |
~19ms | Canvas measureText() + caching (once) |
layout() |
~0.09ms | Arithmetic on cached widths |
| DOM-based | hundreds of ms | getBoundingClientRect × 500 |
0.09ms means 10,000 calls < 1ms. Virtual scroll re-measurements become free.
Two Usage Patterns
1. Height only: prepare() + layout() → virtual lists, overflow detection, responsive layouts
2. Line-level control: layoutWithLines() / walkLineRanges() / layoutNextLine() → Canvas/WebGL rendering, SVG text wrapping, variable-width layouts (text flowing around images), server-side rendering
Multilingual Support
Supports emoji (grapheme cluster segmentation, including compound emoji like 🏳️🌈), CJK (full-width width calculation), Arabic/Hebrew (BiDi RTL text).
Key Caveat (Community Feedback)
Canvas measureText() widths don't perfectly match DOM rendering widths in all cases:
Font hinting: Sub-pixel differences between DOM and Canvas text renderers
letter-spacing/word-spacing: CSS spacing not reflected in Canvas
OpenType features: Ligatures may render differently
line-height: CSS line-height vs Canvas textBaseline use different baselines
Bottom line: "Good enough" measurement, not pixel-perfect. Perfect for virtual scrolls where a few pixels don't matter. May have discrepancies with complex CSS typography.
Creator: chenglou
Early React contributor, core developer of Relay (Facebook's GraphQL client) and Reason (OCaml → JavaScript compiler). Currently at Anthropic. Pretext's high code quality comes from a compiler engineer approaching text layout as a "parsing + optimization" problem.
Key Points
prepare(text, font): Measure each glyph width via Canvas measureText() + cache (once)
layout(prepared, width, lineHeight): Line calculation from cached widths — pure arithmetic, zero DOM, zero reflow
Performance: prepare ~19ms (500 texts), layout ~0.09ms (500 texts) — 10K calls < 1ms
Multilingual: emoji (Grapheme Cluster), CJK (full-width), Arabic (RTL/BiDi) support
Caveat: Canvas measureText() ≠ DOM render result. May differ on letter-spacing, ligatures. "Good enough" measurement