Pretext vs DOM Measurement: Why Pure JS Text Layout Wins

Apr 3, 2026

Every web developer has faced the same problem: you need to know how tall a piece of text will be before you render it. Maybe you are building a virtual scroll, a chat interface, or an editorial layout. The traditional answer is DOM measurement — create a hidden element, insert your text, read back offsetHeight, and discard it. It works, but it is slow, unpredictable, and fragile.

Pretext offers a fundamentally different approach. It computes text layout entirely in JavaScript — no DOM, no reflow, no hidden elements. This post breaks down the two approaches side by side so you can decide which one fits your project.

How DOM Measurement Works

The browser's layout engine is powerful but designed for rendering, not querying. When you need text dimensions, you typically do something like this:

function measureText(text, font, maxWidth) {
  const el = document.createElement('div');
  el.style.cssText = `
    position: absolute;
    visibility: hidden;
    width: ${maxWidth}px;
    font: ${font};
    white-space: pre-wrap;
  `;
  el.textContent = text;
  document.body.appendChild(el);
  const height = el.offsetHeight;
  document.body.removeChild(el);
  return height;
}

This pattern has been the standard for over a decade. It is simple and accurate — but it comes with significant costs.

The Problem with DOM Measurement

1. Forced Reflow

Every time you read offsetHeight, clientWidth, or getBoundingClientRect(), the browser must recalculate layout for the entire document (or at least the affected subtree). This is called a forced synchronous reflow. If you measure 100 items in a loop, you trigger 100 reflows.

2. Main Thread Blocking

DOM measurement is synchronous and runs on the main thread. While the browser is computing layout, nothing else happens — no animations, no event handlers, no rendering. For a list of 1,000 items, this can freeze the UI for hundreds of milliseconds.

3. Hidden Element Side Effects

Inserting and removing hidden elements can trigger unexpected side effects: mutation observers fire, CSS selectors re-evaluate, and scroll positions can shift. In complex applications with third-party libraries, these side effects cause hard-to-debug bugs.

4. No SSR or Worker Support

DOM measurement requires a browser environment. You cannot use it in a Web Worker, in a Node.js server, or during server-side rendering. This limits architectural options for modern applications.

How Pretext Works

Pretext takes a completely different approach. It implements its own text shaping and layout algorithm in pure JavaScript:

import { prepare, layout } from 'pretext';

// Phase 1: One-time setup — measures character widths
const engine = prepare({
  fontFamily: 'Inter',
  fontSize: 16,
  lineHeight: 24,
});

// Phase 2: Layout — pure computation, no DOM
const result = layout(engine, 'Your text here', {
  maxWidth: 320,
});

console.log(result.height); // exact height in pixels
console.log(result.lines);  // array of line break info

The key insight is the two-phase design:

  • prepare() runs once per font configuration. It measures individual character and word widths using the Canvas API (the only DOM touch point). This result is cached.
  • layout() is pure math. Given the cached measurements and a max width, it computes line breaks, text height, and layout geometry. No DOM access whatsoever.

Head-to-Head Comparison

DOM MeasurementPretext
Triggers reflowYes, every callNo
Main thread blockingHeavy (layout + paint)Minimal (pure math)
Batch performanceO(n) reflows for n itemsO(n) computation, zero reflows
Web Worker supportNoYes (after prepare())
SSR compatibleNoPartial (needs prepare() on client)
Hidden elementsRequiredNot needed
AccuracyPixel-perfectSub-pixel accurate
DependenciesBrowser DOMZero dependencies
Bundle sizeN/A (browser built-in)~15 KB gzipped

Performance: Where Pretext Shines

The performance gap between Pretext and DOM measurement grows dramatically with scale.

Single Measurement

For a single text measurement, the difference is negligible. DOM measurement takes about 0.1–0.5ms. Pretext's layout() takes about 0.01–0.05ms (after prepare()). You would not notice this in most applications.

Batch Measurement (100+ Items)

This is where things change. Measuring 500 variable-height items:

  • DOM approach: Each measurement triggers a reflow. Total time: 50–200ms, with visible UI jank.
  • Pretext approach: Pure computation. Total time: 2–10ms, no jank.

The reason is simple: Pretext does not talk to the browser's layout engine. Each layout() call is just arithmetic on cached character widths.

Real-World Example: Virtual Scroll

Virtual scroll components need to know the height of every item to calculate scroll position, even for items not yet rendered. With DOM measurement, you must either:

  1. Render all items once (defeats the purpose), or
  2. Estimate heights and correct them as items scroll into view (causes scroll jumping).

With Pretext, you compute exact heights for all items instantly:

const engine = prepare({ fontFamily: 'Inter', fontSize: 16, lineHeight: 24 });

const heights = messages.map(msg => {
  const result = layout(engine, msg.text, { maxWidth: containerWidth });
  return result.height + padding;
});

// heights[] is exact — no estimation, no correction

When DOM Measurement Is Still the Right Choice

Pretext is not a universal replacement for DOM measurement. Use the DOM when:

  • You need to measure styled HTML, not plain text. Pretext works with text strings, not rendered HTML with mixed fonts, inline images, or complex CSS.
  • You are measuring a single element once. The overhead of prepare() is not worth it for a one-off measurement.
  • You need to account for browser-specific rendering quirks. Pretext's layout algorithm is deterministic, which means it might differ slightly from a specific browser's text rendering in edge cases with complex Unicode or bidirectional text.

When Pretext Is the Clear Winner

Choose Pretext when:

  • You are measuring many items — virtual scrolls, chat logs, content feeds, card layouts.
  • You need measurements off the main thread — pass the prepared engine to a Web Worker.
  • You want predictable performance — no layout thrashing, no reflow storms, no hidden element side effects.
  • You are building a text editor or editorial tool — Pretext's walkLineRanges() API gives you precise line-by-line layout information.
  • You need measurements before rendering — compute layout during data fetching, not after mount.

Migration: From DOM to Pretext

Switching from DOM measurement to Pretext is straightforward. Here is a typical before/after:

Before (DOM)

function getItemHeights(items, containerWidth) {
  const measurer = document.createElement('div');
  measurer.style.cssText = `
    position: absolute; visibility: hidden;
    width: ${containerWidth}px; font: 16px/24px Inter;
  `;
  document.body.appendChild(measurer);

  const heights = items.map(item => {
    measurer.textContent = item.text;
    return measurer.offsetHeight;
  });

  document.body.removeChild(measurer);
  return heights;
}

After (Pretext)

import { prepare, layout } from 'pretext';

const engine = prepare({ fontFamily: 'Inter', fontSize: 16, lineHeight: 24 });

function getItemHeights(items, containerWidth) {
  return items.map(item => {
    const result = layout(engine, item.text, { maxWidth: containerWidth });
    return result.height;
  });
}

The Pretext version is shorter, has no DOM side effects, and runs 10–50x faster for large lists.

FAQ

Does Pretext work with all fonts?

Yes. The prepare() phase uses the Canvas API to measure character widths for any font loaded in the browser. Custom fonts, system fonts, and variable fonts all work.

Can I use Pretext with React, Vue, or Svelte?

Absolutely. Pretext is framework-agnostic. Call prepare() once (e.g., in a module-level constant or a React ref), then call layout() wherever you need dimensions.

Does Pretext handle word wrapping correctly?

Yes. Pretext implements a line-breaking algorithm that handles word boundaries, hyphenation points, and CJK characters. The results match browser rendering for the vast majority of text.

What about canvas.measureText()?

The Canvas API's measureText() gives you the width of a single run of text but does not compute line breaks or multi-line height. Pretext uses measureText() internally during prepare(), then adds its own line-breaking logic on top.

Conclusion

DOM measurement served us well for simple use cases, but it was never designed for high-performance text layout computation. Every measurement forces the browser to stop what it is doing and recalculate layout — a tax that scales linearly with the number of items you measure.

Pretext eliminates this tax entirely. By moving text layout into pure JavaScript, it gives you exact dimensions without reflow, without blocking the main thread, and without hidden elements. For any application that measures text at scale, Pretext is the faster, cleaner, and more predictable choice.

Ready to try it? Visit the Pretext Playground to experiment live, or check out the interactive demos to see real-world examples in action.

Pretext.js Team

Pretext.js Team

Pretext vs DOM Measurement: Why Pure JS Text Layout Wins | Blog