TL;DR
Pretext.js is a zero-dependency TypeScript library for measuring and positioning multiline text through pure arithmetic, not DOM operations. It eliminates forced synchronous reflows and is approximately 500x faster than getBoundingClientRect(). Pretext.js supports every major writing system. If you’re building virtual scrollers, chat UIs, or data grids, this library addresses a longstanding browser performance bottleneck.
Introduction
Whenever your JavaScript calls getBoundingClientRect() or reads offsetHeight, the browser pauses to flush pending style changes, recalculate layout, and force a full rendering pass (forced synchronous reflow)—one of the most expensive browser operations.
Multiply that cost by 1,000 chat bubbles in a virtual list or 10,000 rows in a data grid, and you end up with dropped frames, UI jank, and a poor user experience.
💡 Apidog teams building API-driven frontends know this pain: streaming response data into dynamic UIs while keeping everything smooth is a challenge when your layout engine gets in the way.
Cheng Lou (creator of react-motion, core React/ReasonML at Meta) built Pretext.js to fix this. Shipped in March 2026, it reached 14,000+ GitHub stars within days and became a major topic on Hacker News.
This post breaks down what Pretext.js does, how it works, when to use it, and its limitations, so you can decide if it fits your stack.
What is Pretext.js?
Pretext.js is a pure JavaScript/TypeScript text layout engine. It computes multiline text metrics entirely via arithmetic—no getBoundingClientRect(), no offsetHeight, no reflow, no DOM thrashing.
Instead of asking the browser for text size (which forces a render), Pretext.js calculates it using font metrics from the Canvas API.
Example Usage
import { prepare, layout } from '@chenglou/pretext';
// Step 1: Prepare text (one-time, cacheable)
const handle = prepare('Hello, pretext.js', '16px "Inter"');
// Step 2: Layout at any width (pure arithmetic, microseconds)
const { height, lineCount } = layout(handle, 400, 24);
-
prepare()touches the browser once (CanvasmeasureText()). -
layout()is pure math—fast, reusable, and cacheable.
Why this matters for API-heavy applications
If you’re building apps that consume streaming API responses (AI assistants, dashboards, collaborative editors), you need to know text heights before rendering. Without this, virtual scrollers stutter, chat UIs jump, and users notice. Pretext.js delivers these heights in microseconds, not milliseconds.
The Problem Pretext.js Solves
Forced Synchronous Reflow Explained
When you write:
const elements = document.querySelectorAll('.text-block');
elements.forEach(el => {
const height = el.getBoundingClientRect().height; // REFLOW!
// use height for positioning...
});
Each call to getBoundingClientRect() triggers:
- JavaScript pause
- Style flush
- Layout recalculation
- Value return
This "layout thrashing" means measuring 1,000 elements = 1,000 full recalculations (~94ms, or 6 dropped frames at 60fps).
The Virtual Scrolling Problem
Virtual scrolling libraries (e.g., react-window, tanstack-virtual) need item heights for scroll calculations. For variable-height text, the typical workaround is to render offscreen, measure, and reposition—defeating the purpose of virtualization.
Pretext.js lets you compute exact heights before any DOM nodes exist.
Real Numbers
| Approach | 1,000 text blocks | 500 text blocks |
|---|---|---|
DOM (getBoundingClientRect) |
~94ms | ~47ms |
Pretext.js (layout()) |
~2ms | ~0.09ms |
| Speed difference | ~47x faster | ~500x faster |
The smaller the batch, the bigger the speedup—Pretext.js scales linearly; DOM measurement has constant overhead.
How Pretext.js Works
Three phases:
1. Text Segmentation
prepare() normalizes input text, applies Unicode line-break rules (UAX #14), and segments into breakable units. This supports:
- CJK (Chinese, Japanese, Korean): character-level breaks
- Arabic/Hebrew: RTL and bidi markers
- Thai: dictionary-based segmentation
- Hindi/Devanagari: ligatures and conjuncts
- Emoji: multi-codepoint sequences
-
Soft hyphens: respects
­
2. Canvas Measurement
Each segment is measured via Canvas measureText(), which does not trigger reflow.
const ctx = offscreenCanvas.getContext('2d');
ctx.font = '16px "Inter"';
const metrics = ctx.measureText('Hello');
const width = metrics.width;
Measurements are cached per segment and font.
3. Pure Arithmetic Layout
layout() uses cached segment widths and computes line breaks using a greedy algorithm:
- Sum widths until exceeding container width
- Break to new line
- Repeat
- Multiply lines by line-height for total height
No DOM, no Canvas—just addition and comparison.
The Reusable Handle Pattern
A single prepare() handle works for all container widths:
const handle = prepare(longArticleText, '16px "Inter"');
const mobile = layout(handle, 375, 24);
const tablet = layout(handle, 768, 24);
const desktop = layout(handle, 1200, 24);
Perfect for responsive UIs: measure once, layout instantly at any width.
Practical Use Cases
1. Virtual Scrolling with Variable-Height Text
Integrate Pretext.js with a virtual scroller:
import { prepare, layout } from '@chenglou/pretext';
interface TextItem {
id: string;
content: string;
}
function computeHeights(items: TextItem[], containerWidth: number) {
return items.map(item => {
const handle = prepare(item.content, '14px "Inter"');
const { height } = layout(handle, containerWidth, 20);
return { id: item.id, height: height + 32 }; // +32 for padding
});
}
const heights = computeHeights(chatMessages, 600); // 10,000 items in ~4ms
No off-screen rendering, no estimation, no UI jumps.
2. AI Chat Interfaces
Streaming token-by-token updates? Recompute height every chunk—no DOM required:
let streamedText = '';
const font = '15px "SF Pro"';
socket.on('token', (token: string) => {
streamedText += token;
const handle = prepare(streamedText, font);
const { height } = layout(handle, bubbleWidth, 22);
scroller.updateItemHeight(messageId, height + padding);
});
3. Data Grids with Text Columns
Auto-size columns by measuring cell content:
function computeColumnWidth(values: string[], font: string, padding: number) {
let maxWidth = 0;
for (const value of values) {
const handle = prepare(value, font);
// Layout with infinite width = single line
const { height } = layout(handle, Infinity, 20);
// Use handle's internal width for sizing
maxWidth = Math.max(maxWidth, /* computed width */);
}
return maxWidth + padding;
}
4. Multilingual Content Feeds
Mixed scripts? Same API, accurate results:
const posts = [
{ text: 'This library changed everything', lang: 'en' },
{ text: 'RTL text with correct bidirectional layout', lang: 'ar' },
{ text: 'CJK text gets proper character-level breaks', lang: 'zh' },
];
posts.forEach(post => {
const handle = prepare(post.text, '16px system-ui');
const { height } = layout(handle, 400, 24);
});
Testing Your Text Layout with Apidog
Building text-heavy UIs backed by APIs? Layout is just one side—you also need to verify API responses deliver correct data, formats, and speed.
Apidog makes this easy. Mock streaming responses, test Pretext.js integrations with different text lengths/languages, and verify your virtual scroller before deploying.
For AI chat products, Apidog’s API testing lets you:
- Mock streaming responses to simulate LLM output
- Test with multilingual payloads for layout edge cases
- Validate response schemas for text fields
- Automate test suites for your rendering pipeline
A robust text measurement library is only as good as the quality of your API data.
Known Limitations and Criticisms
Rendering Accuracy Edge Cases
Some users report text extending past bounding boxes in Safari/Chrome. Arithmetic can diverge from browser layout in cases like:
- Unusual kerning pairs
- Mixed font sizes in a block
- Subpixel differences (Canvas vs. DOM)
- Browser-specific shaping quirks
For most virtual scrolling, a few pixels of error is negligible. For pixel-perfect typesetting, it matters.
Canvas Measurement Cost
prepare() still uses Canvas. Creating thousands of unique handles per frame can become a bottleneck—cache and batch where possible. The library doesn’t enforce this.
No CSS Property Support
Pretext.js only measures font specs, not CSS properties:
letter-spacingword-spacingtext-indenttext-transformfont-feature-settingsfont-variant
If your layout depends on these, heights may not match rendered output.
Not a Renderer
Pretext.js only measures—rendering still requires DOM, Canvas, or SVG.
Pretext.js vs. Traditional Approaches
| Feature | Pretext.js | DOM measurement | Estimated heights |
|---|---|---|---|
| Speed (1K items) | ~2ms | ~94ms | ~0ms |
| Accuracy | High | Perfect | Low |
| DOM dependency | None (after prepare) | Full | None |
| Reflow triggers | Zero | One per measure | Zero |
| Multilingual | Full Unicode | Full | Poor |
| CSS property support | Limited | Full | None |
| Memory overhead | Cached segments | DOM nodes | Minimal |
| Responsive layouts | One prepare, many layout | Re-measure/width | Re-estimate/width |
If you need pixel-perfect accuracy and CSS support, use DOM. For speed across thousands of items (with minor sub-pixel differences), Pretext.js is the best option.
Getting Started
Installation
npm install @chenglou/pretext
# or
pnpm add @chenglou/pretext
# or
bun add @chenglou/pretext
Basic Usage
import { prepare, layout } from '@chenglou/pretext';
// Measure a paragraph
const handle = prepare(
'Pretext.js computes text layout without touching the DOM.',
'16px "Inter"'
);
// Get height at a specific width
const result = layout(handle, 600, 24);
console.log(result.height); // e.g., 48 (2 lines x 24px)
console.log(result.lineCount); // e.g., 2
Integration with React
import { prepare, layout } from '@chenglou/pretext';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useMemo, useRef } from 'react';
function VirtualChat({ messages }: { messages: string[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const containerWidth = 600;
const font = '14px "Inter"';
const lineHeight = 20;
const heights = useMemo(() => {
return messages.map(msg => {
const handle = prepare(msg, font);
const { height } = layout(handle, containerWidth, lineHeight);
return height + 24; // padding
});
}, [messages]);
const virtualizer = useVirtualizer({
count: messages.length,
getScrollElement: () => parentRef.current,
estimateSize: (index) => heights[index],
});
return (
<div ref={parentRef} style={{ height: '100vh', overflow: 'auto' }}>
<div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
{virtualizer.getVirtualItems().map(virtualRow => (
<div
key={virtualRow.key}
style={{
position: 'absolute',
top: virtualRow.start,
width: containerWidth,
}}
>
{messages[virtualRow.index]}
</div>
))}
</div>
</div>
);
}
You get a virtual chat UI with accurate heights—no estimation, correction jumps, or reflows.
Interactive Playground
Try the Pretext.js playground to paste text, choose fonts, adjust widths, and verify layout in real time.
When NOT to Use Pretext.js
- Static pages with known content: Native CSS handles layout.
- Pixel-perfect print layouts: Use DOM for ground truth.
-
Heavy CSS text styling: If you use
letter-spacing, etc., Pretext.js may diverge from the rendered result. - Server-side rendering: Pretext.js uses Canvas, which needs polyfills in Node.js. SSR support is planned.
- Small, static lists: For <50 items, DOM measurement is fast enough.
FAQ
Is Pretext.js production-ready?
Shipped in March 2026, 14,000+ GitHub stars, and built by a seasoned frontend engineer. Test with your fonts/content and pin your version.
Does Pretext.js work with React/Vue/Svelte?
Yes. Framework-agnostic—just call prepare() and layout() as needed.
How does Pretext.js handle web fonts?
prepare() measures text using the font loaded at call time. Ensure your web fonts are loaded (use document.fonts.ready) before measuring.
Can I use Pretext.js for Canvas or SVG rendering?
Yes. The computed line breaks and heights can be used for DOM, Canvas, SVG, etc.
Does it support RTL languages?
Yes—Arabic, Hebrew, and other RTL scripts with correct bidirectional support.
What’s the bundle size?
15KB minified, zero dependencies. Uses standard browser APIs (Canvas.measureText(), Intl.Segmenter).
How accurate is it compared to DOM measurement?
Usually within 1-2 pixels. If you use CSS properties not supported by Pretext.js, expect larger differences.
Can Pretext.js measure styled text (bold, italic, mixed sizes)?
Each prepare() handles a single font. For mixed styles, segment and measure separately.
Conclusion
Pretext.js solves a long-standing browser performance issue: fast, accurate text measurement without DOM reflow. If you build virtual scrollers, chat UIs, or data grids, it removes a whole class of workarounds with just two functions.
It’s not a silver bullet—doesn’t handle all CSS properties, has sub-pixel differences, and lacks server-side support. But for precomputing text heights in virtualized lists, nothing comes close.
Ready to build faster text-heavy UIs? Start by testing your API endpoints with Apidog to ensure your data layer is robust, then integrate Pretext.js into your rendering flow.


Top comments (0)