Pretext and the Browser: How Browsers Lay Out Text (and Where Pretext Fits In)

Apr 4, 2026

The browser is the most sophisticated text rendering engine ever built. It handles font loading, glyph shaping, bidirectional text, line breaking, hyphenation, ligatures, and a thousand other typographic details — across every language, every script, every device. And it does all of this behind a wall you cannot see through.

JavaScript can ask the browser to render text. JavaScript can read the result after the browser has rendered it. But JavaScript cannot ask the browser "how tall would this text be at 320 pixels wide?" without actually rendering it. That gap is where Pretext lives.

How the Browser Renders Text

When you put text in the DOM, the browser runs a multi-stage pipeline:

1. Style Resolution — the browser computes the final CSS values for the text node: font family, size, weight, line height, letter spacing, word spacing, white-space handling, word-break rules.

2. Font Matching — the browser walks the font-family list, finds installed or loaded fonts, and resolves font fallbacks. If a character is not in the primary font, the browser searches fallback fonts and eventually the system fallback.

3. Text Shaping — the browser hands the text and font to a shaping engine (HarfBuzz in Chrome and Firefox, CoreText on Safari). The shaper converts characters into positioned glyphs, handling ligatures, kerning, and complex scripts like Arabic and Devanagari.

4. Line Breaking — the browser determines where to break lines. This involves the Unicode Line Break Algorithm, CSS word-break and overflow-wrap rules, and language-specific breaking rules (CJK characters can break between any two characters; Thai has no spaces between words).

5. Layout — the browser positions each line, applies line-height, handles text-indent, and computes the final bounding box. The result is a set of positioned glyph runs with exact coordinates.

6. Painting — the browser rasterizes the glyphs at the correct subpixel positions, applies antialiasing, and composites the result onto the screen.

This entire pipeline runs in native code, optimized over decades, and it is completely opaque to JavaScript.

Inside the browser rendering pipeline — from DOM to pixels

The Black Box Problem

JavaScript has exactly two ways to measure text:

1. DOM Measurement

Create an element, put text in it, add it to the document, and read its dimensions:

const el = document.createElement('div');
el.style.cssText = 'position:absolute;visibility:hidden;width:300px;font:16px/1.5 sans-serif';
el.textContent = text;
document.body.appendChild(el);
const height = el.offsetHeight; // forces synchronous reflow
document.body.removeChild(el);

This works, but it has serious costs:

  • Forces a synchronous reflow. The browser must run the entire layout pipeline — for the whole document, not just your element — before it can return offsetHeight. On a complex page, this can take milliseconds.
  • Requires the DOM. No measurement in Web Workers, Service Workers, or server-side environments.
  • Cannot be batched efficiently. If you need to measure 1,000 items, you force 1,000 reflows (or you batch them with requestAnimationFrame tricks, which adds latency).
  • Causes layout thrashing. Alternating DOM writes and reads in a loop (write text → read height → write text → read height) is the single worst performance pattern in web development.

2. Canvas measureText()

The Canvas 2D API can measure text width:

const ctx = document.createElement('canvas').getContext('2d');
ctx.font = '16px sans-serif';
const metrics = ctx.measureText(text);
console.log(metrics.width); // total width of the text on one line

This avoids reflow, but it has limitations:

  • Measures a single line. There is no wrapping. You get the width of the entire text as if it were on one line.
  • No line breaking. You cannot ask "where would this text wrap at 300px?" — you just get the total advance width.
  • Limited metrics. Modern browsers expose actualBoundingBoxAscent, actualBoundingBoxDescent, and a few other metrics, but not line break positions or multi-line height.
  • Font matching may differ. Canvas font resolution does not always match CSS font resolution, especially for system font stacks and fallback chains.

Neither API gives you what you actually need: multi-line text dimensions without touching the DOM.

What Pretext Replicates

Pretext does not replace the browser's rendering pipeline. It replicates steps 4 and 5 — line breaking and layout — in pure JavaScript, using glyph measurements from step 3 (via canvas.measureText()).

Here is what Pretext computes:

Browser Pipeline StepPretext Equivalent
Style resolutionYou provide the CSS properties (font, whiteSpace, wordBreak, etc.)
Font matchingHandled by the browser during prepare() via canvas.measureText()
Text shapingHandled by the browser during prepare() via canvas.measureText()
Line breakingPretext reimplements this in JavaScript
Layout (positioning)Pretext reimplements this in JavaScript
PaintingYour responsibility — Pretext outputs numbers, not pixels

The key insight: Pretext delegates the hard parts (font matching and text shaping) to the browser, but takes ownership of the parts the browser will not let you access (line breaking and layout computation).

import { prepare, layout } from 'pretext';

// prepare() uses canvas.measureText() internally
// — the browser handles font matching and shaping
const prepared = prepare(text, {
  font: '400 16px/1.5 "Inter", sans-serif',
  wordBreak: 'normal',
  overflowWrap: 'break-word',
});

// layout() reimplements line breaking and positioning
// — pure JavaScript, no browser API calls
const result = layout(prepared, 300);
console.log(result.height); // multi-line height, matching CSS
console.log(result.lines);  // number of lines

Why the Results Match

Pretext measurements match browser layout because both systems process the same inputs with the same rules:

Same glyph widths. Pretext uses canvas.measureText() to measure text segments. The canvas API uses the same font matching and shaping engine as CSS rendering. If the browser shapes "fi" as a ligature in CSS, it shapes it the same way in Canvas.

Same line breaking rules. Pretext implements the CSS line breaking algorithm — white-space, word-break, overflow-wrap, line-break — following the same spec that browsers follow. A line breaks in the same place in Pretext as it does in CSS.

Same line height model. Pretext applies line-height the same way CSS does: each line box has a height of line-height, regardless of the actual glyph ascent and descent (for line-height values greater than the font size).

There are edge cases where measurements may diverge by a sub-pixel amount — font hinting differences between canvas and CSS rendering, or platform-specific font-size-adjust behavior — but for layout purposes (height prediction, line counting, shrink-wrapping), the results are functionally identical.

Browser Compatibility

Pretext relies on two browser APIs:

  1. CanvasRenderingContext2D.measureText() — available in all browsers since IE9. Pretext uses the basic width property, which has universal support. The extended TextMetrics properties (actualBoundingBoxAscent, etc.) are used when available but not required.

  2. Intl.Segmenter — used for grapheme cluster and word segmentation. Available in Chrome 87+, Edge 87+, Safari 15.4+, Firefox 125+. For older browsers, Pretext falls back to a built-in segmentation algorithm.

No other browser APIs are used during layout(). The layout step is pure arithmetic — addition, comparison, and iteration over pre-measured segments. It runs identically in any JavaScript environment.

Performance: Pretext vs. Browser Reflow

The browser's layout engine is written in highly optimized C++. Pretext is written in JavaScript. So why is Pretext faster for text measurement?

Because you are not comparing Pretext against the browser's text layout code. You are comparing Pretext against the entire browser reflow process.

When you read offsetHeight, the browser does not just lay out your element. It must:

  1. Recalculate styles for any dirty elements
  2. Run layout for the entire containing block (and possibly the whole document)
  3. Update the layout tree
  4. Return the computed value

Even if your element is absolutely positioned and off-screen, the browser must still validate that no other layout is dirty before returning a measurement. This overhead dominates.

Pretext skips all of this. It measures only what you ask it to measure — one text string, at one width — with zero overhead from the rest of the page.

Benchmarks (approximate, varies by hardware and page complexity):

OperationTime
Single offsetHeight read (simple page)0.1–0.5ms
Single offsetHeight read (complex page)1–10ms
1,000 offsetHeight reads (layout thrashing)500–5,000ms
Single Pretext prepare() + layout()0.05–0.2ms
1,000 Pretext layout() calls (same prepared text)0.1–1ms

The advantage grows with scale. One measurement is comparable. A thousand measurements is a 1,000x difference.

Web Workers: Layout Off the Main Thread

Because layout() is pure JavaScript with no browser API dependencies, it runs in Web Workers:

// worker.js
import { layout } from 'pretext';

self.onmessage = ({ data: { preparedItems, width } }) => {
  const results = preparedItems.map(item => ({
    id: item.id,
    ...layout(item.prepared, width),
  }));
  self.postMessage(results);
};

The prepare() step still requires canvas.measureText(), so it must run in a context with Canvas access (main thread, or an OffscreenCanvas in a Worker). But once text is prepared, all layout computation can move off the main thread entirely.

This pattern is ideal for virtual scroll — prepare text on the main thread during idle time, then compute layout in a Worker as the user scrolls. The main thread never blocks on text measurement.

What Pretext Cannot Do

Pretext replicates line breaking and layout. It does not replicate:

  • Font loading. You must ensure fonts are loaded before calling prepare(). If the font is not loaded, Canvas will fall back to a default font, and measurements will not match CSS once the real font loads. Use document.fonts.ready or the Font Loading API.
  • Hyphenation. CSS hyphens: auto uses language-specific hyphenation dictionaries built into the browser. Pretext does not include hyphenation dictionaries. If you need hyphenation, apply it to the text before passing it to Pretext.
  • Subpixel rendering. Pretext computes layout at pixel-level precision. It does not predict subpixel glyph positioning or anti-aliasing — but these do not affect layout dimensions.
  • Variable font interpolation. Pretext handles variable fonts correctly (via Canvas measurement), but it does not expose variable font axes or animation.

These are rendering details, not layout details. For the purpose of answering "how tall will this text be?", Pretext gives you what you need.

Putting It Together

The browser is unmatched at rendering text. But its layout results are locked behind a synchronous, whole-page reflow. Pretext extracts the parts you need — glyph measurement, line breaking, and height computation — and makes them available as a fast, synchronous, DOM-free JavaScript API.

Use the browser for what it does best: rendering. Use Pretext for what the browser will not let you do: predicting layout before rendering.

import { prepare, layout } from 'pretext';

// Browser handles font matching and shaping (via canvas)
const prepared = prepare(text, { font: '16px/1.5 sans-serif' });

// Pretext handles line breaking and layout (pure JS)
const { height, width, lines } = layout(prepared, containerWidth);

// Browser handles rendering (via CSS)
element.style.width = `${width}px`;
element.style.minHeight = `${height}px`;
element.textContent = text;

Three lines. No reflow. No double render. No layout thrashing. The browser and Pretext each do what they are best at.

Getting Started

npm install pretext

Explore the interactive examples to see Pretext and the browser working together, or read the docs for the full API reference.


Learn more at pretextjs.dev or explore the interactive demos.

Pretext.js Team

Pretext.js Team

Pretext and the Browser: How Browsers Lay Out Text (and Where Pretext Fits In) | Blog