Last week Cheng Lou, a former React core team member and the creator of React Motion, released Pretext. It is a pure TypeScript library for measuring and laying out text without touching the DOM. It weighs 15 kilobytes, needs zero dependencies, and has full Unicode support. It currently stands at over 14,000 GitHub stars.
At this point it's been covered to death by slop blogs and tweets with ridiculous claims like CSS is dead. But cutting through the hype, what Pretext did do is solve a specific, well-understood performance problem in a clean and thorough way. The solution is quite clever and the library is worth highlighting.
The problem Pretext solves
Why measuring text is expensive
Browsers render pages in phases: style calculation, layout, paint, composite. Layout is where the browser figures out where every element goes and how large it is. When JavaScript reads a layout property like offsetHeight, clientWidth, or getBoundingClientRect(), the browser must complete the layout phase synchronously before returning a value. This is called forced synchronous reflow.
One read costs a few microseconds, but the problem is you rarely need one measurement.
A virtual scrolling list needs the height of every visible row. A data table needs column widths for each cell. A rich text editor needs line counts and caret positions. A chat interface streaming tokens recalculates layout on every chunk. In these situations, measurement happens hundreds of times per frame.
Measuring 500 text blocks via the DOM requires between 15 and 30 milliseconds, which in turn causes 500 individual layout reflows. On Safari, the same operation takes 149 milliseconds.
To maintain 60 frames per second, each frame gets 16.67 milliseconds of total budget. DOM-based text measurement for 500 items eats the entire budget on Chrome and blows past it by 9x on Safari. There is nothing left for rendering, animation, or user interaction.
On mobile, the numbers get worse. A single forced reflow on a mid-range Android phone blocks the main thread for 10 to 100 milliseconds. The app feels broken, and the developer testing on a MacBook Pro sees nothing wrong.
Virtual scrolling libraries like react-window and tanstack-virtual exist specifically because rendering 10,000 rows at once destroys performance. But even these libraries need row heights. If your rows contain variable-length text, you are back to measuring. And measuring means reflows.
Chat applications hit the same wall. An AI assistant streaming tokens into a message window triggers a reflow on every chunk. At 20 tokens per second, the browser is reflowing 20 times per second on top of everything else the UI is doing.
Why "batch your reads" only gets you so far
The usual advice is to batch your DOM reads before DOM writes. This avoids interleaving reads and writes, which is good practice. But if you are measuring 500 items, the browser still has to run the full layout pipeline. On Safari, this takes upwards of 140ms, effectively nuking your entire performance budget. Batching helps. It does not eliminate the cost.
Some teams use hidden off-screen elements. This helps with visual stability but does nothing for performance. The browser still runs layout on the hidden element.
Others estimate text height from character count or font size. This works for fixed-width fonts and single-line labels. It falls apart with proportional fonts, multilingual content, or anything involving line wrapping. A 5% estimation error on a 1,000-item list means the scrollbar position is wrong by 50 items.
The DOM was never designed for fast, repeated text measurement. It was designed for document rendering. Every measurement request goes through the full layout pipeline because the browser has no lighter-weight path to give you text dimensions.
How Pretext works under the hood
The core idea is simple enough to sound wrong: stop asking the browser for text dimensions entirely. Read font metrics once through the Canvas API, then do all layout math yourself.
Two functions
The prepare() function normalizes your whitespace and segments your text using Intl.Segmenter to understand Unicode boundaries. It then measures each segment through the Canvas measureText() API. Every measurement gets cached by a (segment, font) key.
This runs once and caches the results. Since Canvas measurement does not trigger a layout reflow, it skips the expensive browser pipeline entirely. For 500 paragraphs, prepare() takes about 17 milliseconds.
The layout() function takes those cached measurements and a maxWidth value, then calculates line breaks through pure arithmetic. It accumulates segment widths until the line overflows, breaks to the next line, and continues. No DOM access. No reflow.
Because this phase is pure math, it is significantly faster than the usual approach, finishing in roughly 0.09ms on Chrome compared to nearly 45ms for traditional DOM measurement. On Safari, the gap is 0.12 milliseconds versus 149 milliseconds.
In absolute terms: your 500-paragraph layout fits comfortably inside a single frame with 16 milliseconds to spare. You run it on every resize event or on every keystroke in a text editor. It never touches the DOM, so it never blocks the rendering pipeline.
The Unicode part
As Lou puts it, he "crawled through the depths of hell" to get line breaking right across every writing system. This deserves more attention because it is where most text measurement approaches fall apart in production.
A simple character-count heuristic breaks on Chinese, Japanese, and Korean, where each character is a valid line break point. It breaks on Arabic and Hebrew, where text flows right-to-left and mixes with left-to-right content in the same paragraph. It breaks on Thai, which has no spaces between words. And it breaks on emoji sequences, where a family emoji is technically 7 Unicode code points rendered as a single glyph.
Pretext uses Intl.Segmenter for grapheme and word boundary detection, which means it follows the Unicode specification for every supported writing system. CJK gets per-character breaks. Arabic gets correct bidirectional handling. Emoji sequences stay intact.
Lou reportedly set up recursive testing loops where AI assistants wrote the line break logic, tested it against actual text in actual browsers across different operating systems, compared the results, and iterated until the output matched pixel-for-pixel. For weeks. This is the kind of engineering effort a "text measurement library" description undersells. Getting English line breaks right is a weekend project. Getting 20 writing systems right across 3 browser engines is a different category of work.
Caching
Pretext caches font measurements by segment and font combination. If you measure 500 paragraphs in 16px Inter, and then measure 500 more in the same font, the second batch skips canvas measurement entirely. The cache already has the glyph widths.
A chat window receiving new messages does not re-measure the entire history. It measures the new message against the existing cache and gets results in microseconds. A responsive layout recalculating on window resize runs layout() with a new maxWidth against the same cached segments. No re-preparation needed.
The library also handles soft hyphens correctly. If a hyphen wins a break point, the trailing hyphen character appears in the output. This is a CSS spec detail most measurement libraries ignore, but it matters for editorial tools and long-form content where hyphenation affects line count.
The API
Three levels of control are available.
measureNaturalWidth() gives you the width of the text as it would appear on a single line. This is useful for tooltips, badges, or any other element where you need to know the intrinsic width of the content.
layoutWithLines(maxWidth) gives you an array of line objects. Each object includes a start index, an end index, and the line's width. This is the primary API for most situations. Just provide the width of your container, and you'll receive the line structure in return.
walkLineRanges(maxWidth, onLine) is the low-level callback API. It walks through line break points without allocating arrays. Use this for per-line processing in a tight loop, like rendering to Canvas or calculating cumulative heights for virtual scrolling.
What Pretext enables
Something CSS flat-out does not support: text layout driven by arbitrary geometry, updated every frame, without destroying performance. This opens the door to more creative, editorial-style layout options on the web.
The practical use cases are less flashy but more broadly relevant. Virtual scrolling with accurate row heights. Data tables where column widths adjust to content. Editors where you need caret positioning without triggering reflow. Auto-sizing text areas. Tooltip positioning. Dynamic chart labels. Any situation where you need to know the dimensions of text before or without rendering it to the DOM. If you are building something where scroll performance, dynamic text layout, or accurate pre-render measurement matters, Pretext is worth adding to the stack.













