DEV Community

Cover image for Pretext.js: The 15KB Library That Makes Text Layout 500x Faster
Wanda
Wanda

Posted on • Originally published at apidog.com

Pretext.js: The 15KB Library That Makes Text Layout 500x Faster

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.

Try Apidog today

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.

Pretext.js demo

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);
Enter fullscreen mode Exit fullscreen mode
  • prepare() touches the browser once (Canvas measureText()).
  • 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...
});
Enter fullscreen mode Exit fullscreen mode

Each call to getBoundingClientRect() triggers:

  1. JavaScript pause
  2. Style flush
  3. Layout recalculation
  4. 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;
Enter fullscreen mode Exit fullscreen mode

Measurements are cached per segment and font.

3. Pure Arithmetic Layout

layout() uses cached segment widths and computes line breaks using a greedy algorithm:

  1. Sum widths until exceeding container width
  2. Break to new line
  3. Repeat
  4. 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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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 mock streaming

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-spacing
  • word-spacing
  • text-indent
  • text-transform
  • font-feature-settings
  • font-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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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)