rotem-horovitz
    _hello_about-me_blog_snippets_projects_games
_contact-me
find me in:
privacy
← Back to blog

Can pretext Replace DOM Text Measurement, and Should It?

March 31, 2026 | 7 min read
performancejavascripttypography

I'll be honest. When I first saw pretext making the rounds, my reaction was skepticism. Another library claiming CSS is obsolete? Another "revolutionary" approach that turns out to be a solution hunting for a problem? I've seen enough of those.

But the stars kept climbing, the README was unusually blunt about its own limitations, and the author is Cheng Lou, who built React Motion and has a track record of shipping things that matter. So I did what I do when hype outpaces my understanding. Dig in, benchmark it, work out where this thing earns its keep.

What follows is that investigation. Every benchmark runs live in your browser. Every demo uses the real library. And the verdict lands somewhere messier than "use it" or "don't."


01. The problem

Why measuring text height is surprisingly hard

Here's something most developers don't think about until it bites them: asking the browser "how tall is this text?" is one of the most expensive things you can do.

The moment you ask, the browser stops what it's doing and recalculates the size and position of elements on the page. That process is called layout reflow, and only once it finishes do you get your answer. For one element you'll never notice. Ask for hundreds at once, in a scrolling feed or a chat window or a Pinterest-style grid, and those tiny pauses add up fast.

The interleaving trap

It gets worse if you're not careful. The classic mistake is alternating between changing the DOM and reading measurements in a loop. Each read forces the browser to recalculate everything you just changed before it can answer. Then you change something again, and the cycle repeats.

INTERLEAVED PATTERN: each component reads independently
Component A
write DOM
read ⚡
REFLOW
Component B
write DOM
read ⚡
REFLOW
DOM write
getBoundingClientRect / offsetHeight
forced synchronous layout

Even the "smart" approach, saving all your changes first and then reading all your measurements in one batch, still triggers a full recalculation. For 500 items on a resize event that's about 0.18ms in Chrome. Not catastrophic, but it happens on every resize, and it ties your layout code to the browser's rendering schedule.

The real issue isn't speed. It's dependency. To measure text with the DOM, the element has to exist on the page first. You can't measure in a background thread. You can't measure on the server. You can't measure before the component mounts. That coupling is the constraint.

What developers do instead

Most teams just... work around it. Average row heights, close enough. Estimates from character count, not great. Hidden elements to measure against, expensive. Or they accept the jank. Some libraries get clever. canvas-hypertxt, from the Glide team, trains a model to estimate string widths. uWrap uses character-pair kerning tables for Latin text.

These all share one thing: they're approximations. Pretext claims to be exact. Bold claim. Let's find out.


02. How it works

The big idea: skip the DOM entirely

Pretext's core insight is straightforward. Instead of putting text in the DOM and asking the browser to lay it out, it uses the canvas text API (measureText()) to get word widths straight from the font engine. No layout, no reflow, no DOM. It measures everything once, caches the results, and from then on calculates text height with arithmetic.

✗ DOM approach
// Forces layout reflow every call
function getHeight(text, width) {
  el.style.width = width + 'px';
  el.textContent = text;
  return el.getBoundingClientRect().height;
  // triggers full layout reflow
}

~0.18ms per batch (500 items, resize)

✓ Pretext approach
// One-time prep: canvas measureText
const prepared = prepare(text, '16px Inter');

// Hot path: pure arithmetic
const { height } = layout(prepared, width, 24);
// no DOM, no reflow, ~0.0002ms

~0.02ms for all 500 items on resize

Phase 1: prepare(), measure once

This is the setup step. Pretext takes your text, breaks it into words and segments with Intl.Segmenter so Chinese, Arabic, and emoji all split correctly, then measures each piece with canvas.measureText(). The results get cached by font. Processing 500 paragraphs takes about 19ms, and you pay that once.

Phase 2: layout(), arithmetic instead of measurement

This is the fast part. Given the cached word widths, a container width, and a line height, pretext just... does arithmetic. It walks through the widths, figures out how many lines the text wraps to, and returns the height. No canvas calls. No DOM. No string processing. Just addition and comparison. All 500 items in 0.09ms.

19ms
prepare() · 500 texts · once
0.09ms
layout() · 500 texts · hot path
100%
accuracy · named fonts · curated corpus
7680
test cases · 4 fonts × 8 sizes × 8 widths × 30 texts

The 100% accuracy figure covers named fonts with standard CSS wrapping on the library's curated test corpus. Arabic, Thai, and Myanmar have edge cases, covered in Caveats below. One gotcha: don't use system-ui on macOS. Canvas and DOM resolve it to different SF Pro variants at certain sizes, which breaks the measurements. Stick with named fonts like Inter or Helvetica Neue.


03. Live benchmark

See for yourself

This is where I stopped being skeptical. The numbers below aren't cherry-picked from a README. They run live in your browser, on the real DOM APIs and pretext's actual code. Hit Run and see what your machine does.

Text Height Prediction
Mixed paragraphs, 2 to 8 sentences each. The resize simulation runs layout at 5 widths, from 280px to 700px.
DOM interleaved (worst)
—
DOM batched (best case)
—
Pretext prepare(), once
—
Pretext layout(), hot path
—

Click Run to start. Tests run sequentially to avoid interference.

"DOM batched" is the best-case DOM strategy: all writes first, then all reads in one pass. "DOM interleaved" is the common mistake of reading after each write. "Pretext layout()" is the hot-path cost after the one-time prepare() step.

What happens when the window resizes?

A single measurement pass is one thing. In practice the number that matters is the resize cost, what happens every time someone drags their browser window. With the DOM you re-measure everything from scratch. With pretext you call layout() again with the new width. Same cached data, different arithmetic. The gap widens with every resize event.

Resize Simulation, 5 width changes
DOM (per resize event)
—
Pretext layout() (per resize)
—

Run the main benchmark first.


04. Real-world scenarios

Beyond synthetic benchmarks

Numbers in a chart are nice. I wanted to see pretext solve the problems that have cost me real time in production, not toy demos. Here are four scenarios I tested, each with a live demo.

Virtualised lists with variable-height rows

Virtual scrolling renders only what's visible, offsetting content to simulate a large list. The hard part: computing each row's height to know the total scroll height and which rows are currently visible. With fixed-height rows this is trivial. With text of varying length it has always required either sampling the DOM or accepting wrong estimates.

The conventional approach uses a ResizeObserver or a one-time measurement pass with hidden elements. Both need the elements to exist in the DOM before you know their height, which forces a two-phase render or eager DOM population. Pretext breaks that dependency.

✗ Traditional virtual scroll
// Must pre-render items to measure
const heights = items.map(item => {
  ghost.textContent = item.text;
  return ghost.getBoundingClientRect().height;
  // Reflow for every item
});
totalHeight = heights.reduce((a,b) => a+b);
✓ Pretext virtual scroll
// Measure once, reuse forever
const prepared = items.map(item =>
  prepare(item.text, font)
);
const heights = prepared.map(p =>
  layout(p, columnWidth, lineH).height
);
// No DOM. Runs in a Worker.
Verdict: strong use case. Pretext was built for this. The prepare/layout split maps straight onto virtualised list architecture. Prepare when data loads, layout when width changes. Running it in a Web Worker is a real bonus.

05. Impossible things

Things pretext can do that CSS literally cannot

This is the section that changed my thinking. Pretext isn't a faster way to do what CSS already does. It does things with no CSS equivalent at all. The browser's layout engine is powerful, and you can't program it. Pretext turns text layout into a function call, and that opens doors.

MULTILINE SHRINKWRAP
The tightest container that still wraps to N lines. No CSS property exists for this.
WORKER-SIDE LAYOUT
Compute heights off the main thread. DOM measurement requires the UI thread.
CANVAS / SVG TEXT
Route wrapped text to Canvas or SVG with exact line positions. CSS layout doesn't exist there.
OBSTACLE ROUTING
Flow text around images with per-line width changes. CSS shape-outside is limited; JS shapes aren't.
BALANCED TEXT
Find the width where lines are most balanced (headline widows). CSS text-wrap: balance approximates this.
BUILD-TIME CHECKS
Verify that button labels don't overflow their containers in CI, no browser required.

06. Caveats

The honest trade-offs

Time to put the skeptic hat back on. Every tool has limits, and you should know these before you adopt pretext.

It only models one CSS mode

Pretext currently assumes white-space: normal, word-break: normal, overflow-wrap: break-word, and line-break: auto. If your text uses nowrap, break-all, custom tab sizes, or CSS columns, the predictions will be wrong.

Your CSS font must match exactly. Pass the same font string to prepare() that you use in CSS: "600 16px/1 Inter", not just "Inter". Weight, size, and line height all affect character widths. A mismatch produces silently wrong results, which is the worst kind of bug.

system-ui is broken on macOS

Canvas and DOM resolve system-ui to different SF Pro variants (Text vs Display) at certain sizes on macOS. The mismatch can reach 14.5%. Just use a named font and you're fine.

Multilingual accuracy has gaps

The 100% accuracy claim (7,680 tests across 4 fonts, 8 sizes, 8 widths, 30 texts) is real. The corpus is also curated. Some known edge cases:

  • Arabic. Clean on coarse tests, ~7 out of 601 edge cases remain in fine sweeps.
  • Japanese. Narrow-width font compression, not a segmentation bug.
  • Myanmar. Chrome and Safari disagree on closing-quote and follower cluster behavior.
  • Thai. 59/61 on the coarse sweep, two tiny misses remain.
  • Hebrew. Not covered in the accuracy sweeps at all. The library has BiDi support with heuristic direction detection, but mixed Hebrew and English text, which is everywhere in Israeli tech, is uncharted. Test it yourself if that's your stack.

Rich text takes more work

Out of the box, pretext measures a single run of text in a single font. Mixed formatting, so bold words, links, and inline code, isn't built in. It is possible: the official rich-note demo prepares each styled span with its own font and flows them together with layoutNextLine(). It works, but you manage per-span preparation and fragment assembly yourself. Doable, not trivial.

Canvas rendering hurts accessibility

This is the caveat that deserves more attention. Use pretext for measurement only, predicting heights for virtual scrolling, masonry, or shrinkwrap, and your text still lives in the DOM as regular HTML. Screen readers, text selection, find-in-page, user font preferences, all fine. Zero accessibility cost.

The moment you render text to Canvas, as the typography demo does, all of that disappears. Canvas text is invisible to assistive technology. It can't be selected, searched, copied, or read aloud. To a screen reader that paragraph doesn't exist.

You can mitigate it with hidden DOM text, aria-label, or transparent overlays the way PDF.js does, and none of that is free. For measurement, pretext is accessibility-neutral. For canvas rendering you're building a custom rendering engine, and you inherit every a11y obligation that comes with one.

It's brand new

As of March 2026, pretext is on npm as @chenglou/pretext (v0.0.3), and it's days old. The first release landed March 26. The API looks stable, and it hasn't seen the volume of production use that shakes out the weird edge cases.


07. When to use it

A practical cheat sheet

After all the testing, here's my take on when pretext justifies the added complexity, and when CSS is the right call.

ScenarioUse Pretext?Notes
Virtual scroll, 100+ variable-height rowsYESCore use case. Prepare on data load, layout on resize.
Chat bubbles with shrinkwrapYESCSS cannot do this. walkLineRanges() + binary search.
Canvas / WebGL text renderingYESlayoutWithLines() gives exact line positions for fillText().
Masonry layout (text cards only)YESHeight prediction before mount. Images still need size data.
Editorial / balanced justificationTEST FIRSTlayoutNextLine() and layoutWithLines() enable per-line-width algorithms, but Canvas/SVG-rendered text is invisible to screen readers. Extra effort to maintain an accessible DOM fallback may not be worth it.
Accordion / expand-collapse animationsTEST FIRSTWorks, but CSS height: auto + transition usually suffices.
Scroll position anchoring on new contentTEST FIRSTUseful if you need height before insertion. CSS overflow-anchor also helps.
Standard text in a fixed-width containerNOLet the browser lay it out. No reason to override.
Rich text (mixed fonts/weights inline)TEST FIRSTCore API is single-font, but per-span measurement via layoutNextLine() is possible (see rich-note demo). Non-trivial.
CSS Grid / Flexbox sizingNOBrowser layout engine handles this better than any JS.
Multilingual app with Arabic/Myanmar/ThaiTEST FIRSTLibrary has known edge cases in fine sweeps. Validate your specific content.

How to think about it

Pretext is a companion to your render cycle, not a replacement for CSS. Reach for it when you need to know text dimensions before mounting, or when you're rendering outside the DOM entirely. For everything else, the browser's layout engine is still the best tool for the job.

// Good pattern: prepare() when data arrives
function onDataLoaded(items) {
  preparedItems = items.map(item => ({
    ...item,
    prepared: prepare(item.text, '16px Inter'),
  }));
  updateLayout(containerWidth);
}

// Good pattern: layout() on every resize
function updateLayout(width) {
  offsets = [];
  let y = 0;
  for (const item of preparedItems) {
    offsets.push(y);
    y += layout(item.prepared, width, 24).height + GAP;
  }
  totalHeight = y;
}

08. Verdict

So... should you use it?

The short answer

If you're building virtual scrolling, Canvas/SVG text rendering, chat UIs, or any layout that requires knowing text height before mounting, yes, pretext is the best tool available. The performance lead over DOM measurement is real, the accuracy held up on everything I threw at it, and the API is small enough to learn in an afternoon.

If you're building standard content pages, forms, or anything where the browser's own layout engine can do the work, don't reach for pretext. The additional complexity, font-sync discipline, and CSS-mode restrictions aren't worth it when CSS already has the answer.

What changed my mind

I came into this expecting to write a "CSS is fine, you don't need this" article. For most use cases that's still true. CSS is fine, and you don't need this.

But pretext isn't trying to replace CSS. It solves the problems where the browser's layout engine can't help, because the elements don't exist yet, because you're off the main thread, because you need the answer before the DOM is ready. I've hit all three in production and always solved them with hacks. Pretext replaces the hacks with arithmetic.

The library's real contribution isn't speed. It's decoupling text measurement from the DOM lifecycle. That's what makes the "impossible things" possible: worker-side layout, build-time overflow checks, canvas text routing, multiline shrinkwrap.

What to watch

It needs a stable 1.0, framework adapters, and more production mileage before it's an obvious default. Multilingual accuracy should improve as more real text gets thrown at it.

Credits

Cheng Lou built pretext. He also built React Motion and has contributed to the React ecosystem for years. The interactive demos in this article take their cue from the official pretext demos.