Library Overview

The Pretext Library: Pure-JavaScript Text Layout Without the DOM

Pretext is a pure-JavaScript text layout library. It measures multiline text, computes line-break positions, and reports geometry — width, height, line count, per-line bounds — without ever touching the DOM. The whole library is roughly 15KB minified, has zero runtime dependencies, and is roughly 200×–500× faster than equivalent DOM-based measurement on representative workloads.

That's the executive summary. The rest of this page is for engineers who need to decide whether to adopt it, integrators looking for the API at a glance, and people who landed here from search and just want to know what the thing is.

The library was created by Cheng Lou — formerly on the React core team, author of react-motion, contributor to ReScript and Midjourney — and lives at github.com/chenglou/pretext. The npm package is @chenglou/pretext.

What It Is, in One Sentence

Pretext separates text layout into two phases: a one-time measurement phase that uses the browser's actual font engine via off-screen canvas measureText() calls, and a layout phase that runs as pure arithmetic on the cached measurement data. The first is fast enough to do once per (text, font) combination. The second is fast enough to call inside requestAnimationFrame for every visible row of a long list.

What Problem It Solves

Browsers measure text by laying it out. Every call to getBoundingClientRect(), offsetHeight, or getComputedStyle() on a text node forces the browser to commit a layout reflow — synchronous, blocking, and proportional in cost to how much else has changed since the last commit. For a single label, this cost is invisible. For 500 labels in a virtual scroller resizing as the user drags a column boundary, this cost becomes the dominant frame budget item. You drop frames not because rendering is slow but because measurement is slow.

The traditional workarounds — debouncing measurements, hoisting them out of the render loop, caching computed heights manually, or guessing heights and correcting later — all trade correctness or simplicity for performance. None of them eliminate the underlying cost.

Pretext eliminates it. It builds its own measurement pipeline, computes layout as math, and never asks the DOM what size anything is.

API at a Glance

The library's full public surface is small enough to print on a card:

import {
  prepare,
  layout,
  layoutWithLines,
  prepareWithSegments,
  walkLineRanges,
  layoutNextLine,
  clearCache,
  setLocale,
} from '@chenglou/pretext';

The two functions you'll use 95% of the time are prepare() and layout(). Everything else is for advanced cases.

prepare(text, font, options?)

Called once per unique (text, font) pair. Tokenizes the text using Unicode segmentation, identifies break opportunities, measures glyph widths through the canvas font engine, and returns a small opaque object — the "prepared" representation. The font parameter is a canvas font string: "16px Georgia", "600 18px 'Helvetica Neue'", "italic 14px serif". Call clearCache() if you swap fonts at runtime and need to free measurement memory.

const prepared = prepare("Hello, world — this is Pretext.", "18px Georgia");

prepare() is the only "expensive" call in the API, and even then, ~19ms is enough to handle 500 distinct text strings on a developer-class laptop. For a virtual list, you'll call prepare() once per row when its content first becomes known, and never again unless the content changes.

layout(prepared, maxWidth, lineHeight)

The hot path. Pure arithmetic over cached data. Returns { height, lineCount } for a given width and line height. Microsecond-class — call it on every animation frame for every visible row without blowing the frame budget.

const { height, lineCount } = layout(prepared, 400, 1.5);

If you need per-line geometry for rendering, use layoutWithLines() instead, which returns { height, lineCount, lines } with each line's text, width, and character range. If you need to walk lines without allocating strings, walkLineRanges() calls a callback with (start, end, lineIndex) for each line — useful for canvas renderers that draw substrings directly.

prepareWithSegments(text, font, options?)

A richer variant of prepare() that also exposes the segmentation data — break opportunities, character clusters, segment boundaries. Use this for tooling that needs to introspect why the engine broke a line where it did, or for rendering that needs to color or style individual segments.

layoutNextLine(prepared, start, maxWidth)

For variable-width layouts, like text that flows around an obstacle. Lay out one line at a time at whatever width is available at that vertical position. The function returns { end, width } — the character index where the line ended and how wide it actually is. Loop until you've consumed the prepared text.

setLocale(locale?)

Some scripts have locale-sensitive line-break rules. Japanese, for example, has different break behavior depending on whether the surrounding context is "Japanese-style" or "Western-style." setLocale() configures the engine globally (it's an engine-level setting, not per-call). Pass no argument to reset to the platform default.

clearCache()

Frees the measurement cache. Call it after font swaps or when you've prepared transient text you don't expect to re-render.

What It Renders To

Nothing. The library is a measurement and layout engine, not a rendering engine. Once you have layout data, you're free to render the result however you like:

The decoupling is intentional. A layout engine that imposes a renderer is a layout engine that locks you out of one of the four targets above.

Performance: The Number That Started It All

The headline number — "500x faster than DOM" — comes from a controlled benchmark with 500 short strings, measuring the time to compute total height of all of them. Traditional DOM measurement, using getBoundingClientRect() per string in a loop, takes roughly 19ms on a developer-class laptop because each call forces a reflow. Pretext's layout() over the same 500 prepared strings takes roughly 0.09ms.

The honest framing of this number is two-part. First, the speedup is not from cleverer arithmetic — it's from removing reflows. Second, the size of the speedup is workload-sensitive: for a single string the difference is invisible; for hundreds of strings being remeasured per frame the difference is the entire frame budget.

On modern CPUs, both numbers scale roughly linearly with string count and total character count. The cache built by prepare() is the variable that matters most: if you re-prepare on every frame instead of caching the prepared object, you've thrown away most of the win.

Comparison With Adjacent Libraries

Pretext is sometimes confused with three other categories of library. They're worth distinguishing:

Library / Category What it does Overlap with Pretext
Yoga (Facebook) Cross-platform flexbox layout engine None — Yoga lays out boxes, Pretext measures text inside one
fontkit / opentype.js Font file parsing, glyph extraction None — these read fonts, Pretext uses the browser's already-loaded font
Harfbuzz / harfbuzz-js Complete glyph shaping engine Partial — both shape complex scripts; Pretext leans on the browser's harfbuzz instead of bundling its own
CSS white-space and word-break Browser-native text layout Direct — Pretext computes the same answer CSS would, but predictively and without DOM cost
react-virtualized / virtua / TanStack Virtual Virtual scrolling primitives Complementary — virtual scrollers benefit from Pretext for accurate variable row heights

The single sentence: Pretext fills the gap between "the browser will tell me text dimensions if I render it" and "I want text dimensions without rendering it."

Languages and Scripts Supported

Pretext handles 12+ writing systems with correct line-break behavior:

The library does not include its own fonts or font files. It uses whatever font you pass via the canvas font string, which means it inherits whatever language coverage the fonts loaded in the browser have. If you need a glyph the browser can't render, you'll see the fallback box — same as in any text rendering layer.

Installing and Getting Started

npm install @chenglou/pretext

The package is ESM-first, with a CJS fallback for Node-side usage. There are no peer dependencies, which means it works in any framework or no framework. A first integration looks like:

import { prepare, layout } from '@chenglou/pretext';

const prepared = prepare("Multi-line text to measure.", "16px sans-serif");
const { height, lineCount } = layout(prepared, /* maxWidth */ 320, /* lineHeight */ 1.5);

console.log(height, lineCount);

That's the whole API contact for the simple case. From here, you can build virtual scrollers that never reflow, animations that move text around without measurement jitter, layouts that flow around obstacles, or just remove the half-dozen getBoundingClientRect() calls that have been your mystery frame-budget killer.

What to Read Next

If you want to understand the engine's internal architecture, the How Pretext Works deep dive is the next stop. If you want to see the library in motion, the 18-demo community showcase is the fastest tour. If you want to compare it against traditional DOM measurement on your own machine, the benchmark walkthrough has reproducible numbers. And if you're integrating into a React codebase specifically, the Pretext + React guide covers the hook patterns and SSR caveats.

The library is at github.com/chenglou/pretext. The package is at npm @chenglou/pretext. The author is at chenglou.me.


pretext.cool is a community-maintained showcase, not affiliated with Cheng Lou or the official Pretext project.

Related Pages

Try a Live Demo

← Browse all 20 demos