📐

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

1

prepare(text, font): Measure each glyph width via Canvas measureText() + cache (once)

2

layout(prepared, width, lineHeight): Line calculation from cached widths — pure arithmetic, zero DOM, zero reflow

3

Performance: prepare ~19ms (500 texts), layout ~0.09ms (500 texts) — 10K calls < 1ms

4

Multilingual: emoji (Grapheme Cluster), CJK (full-width), Arabic (RTL/BiDi) support

5

Caveat: Canvas measureText() ≠ DOM render result. May differ on letter-spacing, ligatures. "Good enough" measurement

Use Cases

Pre-calculating item heights for virtual scroll lists Detecting overflow when AI-generated text fits inside buttons/cards Rendering text directly in Canvas/WebGL/SVG without DOM