DEV Community

Cover image for Pretext.js: 15KB Bibliothek für 500x schnelleres Textlayout
Emre Demir
Emre Demir

Posted on • Originally published at apidog.com

Pretext.js: 15KB Bibliothek für 500x schnelleres Textlayout

Kurz gesagt

Pretext.js ist eine TypeScript-Bibliothek ohne Abhängigkeiten, die mehrzeiligen Text rein arithmetisch statt mit DOM-Operationen misst und positioniert. Damit eliminiert sie erzwungene synchrone Reflows, liefert ca. 500x schnellere Textmessung als getBoundingClientRect() und unterstützt jedes wichtige Schriftsystem weltweit. Für virtuelle Scroller, Chat-UIs oder Datengrids löst diese Bibliothek ein Problem, das Browser seit Jahrzehnten ignorieren.

Probiere Apidog noch heute aus

Einführung

Jedes Mal, wenn Ihr JavaScript getBoundingClientRect() aufruft oder offsetHeight liest, stoppt der Browser alles: Ausstehende Stiländerungen werden geleert, das Layout neu berechnet und ein vollständiger Rendering-Durchlauf erzwungen. Das ist der berüchtigte synchrone Reflow – die teuerste Operation, die ein Browser ausführen kann.

Multiplizieren Sie das mit 1.000 Chatblasen in einer virtuellen Liste oder 10.000 Zeilen in einem Datengrid: Das Ergebnis sind verlorene Frames, Ruckeln und User, die denken, die App ist kaputt.

💡 Apidog-Teams, die API-gesteuerte Frontends entwickeln, kennen diesen Schmerz: Antwortdaten in dynamische UIs zu streamen und dabei alles performant zu halten, ist eine stetige Herausforderung, wenn die Layout-Engine blockiert.

Cheng Lou (React-Motion, ReasonML), entwickelte Pretext.js, um dieses Problem fundamental zu lösen. Seit Release (März 2026) schoss das Projekt auf GitHub auf über 14.000 Sterne und wurde heiß auf Hacker News diskutiert.

In diesem Beitrag erfährst du, wie Pretext.js funktioniert, wann und wie du es einsetzt – praxisorientiert, mit Code und konkreten Anwendungsfällen.

Was ist Pretext.js?

Pretext.js ist eine reine JavaScript/TypeScript-Textlayout-Engine. Sie misst und positioniert mehrzeiligen Text ausschließlich durch Arithmetik – ohne getBoundingClientRect(), offsetHeight, Reflow oder DOM-Thrashing.

Pretext.js Beispiel

Statt den Browser zu fragen „Wie hoch ist dieser Text?“, berechnet Pretext.js die Antwort mathematisch – mit Schriftmetriken aus der Canvas-API.

API-Überblick:

import { prepare, layout } from '@chenglou/pretext';

// Schritt 1: Text vorbereiten (einmalig, cachebar)
const handle = prepare('Hello, pretext.js', '16px "Inter"');

// Schritt 2: Layout bei beliebiger Breite (reine Arithmetik, Mikrosekunden)
const { height, lineCount } = layout(handle, 400, 24);
Enter fullscreen mode Exit fullscreen mode

Das ist alles. Zwei Funktionen: prepare() misst Textsegmente (per Canvas, einmalig), layout() berechnet das Layout (rein arithmetisch, blitzschnell). Alles nach dem ersten prepare() läuft ohne jeglichen DOM-Kontakt.

Warum ist das für API-intensive Anwendungen relevant?

Wenn du Apps baust, die z.B. Streaming-API-Antworten verarbeiten (KI-Assistenten, Dashboards, kollaborative Editoren), musst du die Höhe von Text kennen, bevor du ihn renderst. Sonst springt dein virtueller Scroller, Chat-UIs ruckeln – und das merkt jeder User.

Mit Pretext.js bekommst du die Höhe in Mikrosekunden, nicht in Millisekunden.

Das Problem, das Pretext.js löst

Erzwungener synchroner Reflow: Das eigentliche Bottleneck

Beispiel:

const elements = document.querySelectorAll('.text-block');
elements.forEach(el => {
  const height = el.getBoundingClientRect().height; // REFLOW!
  // Positionierung ...
});
Enter fullscreen mode Exit fullscreen mode

Jeder getBoundingClientRect()-Aufruf löst Folgendes aus:

  1. JS-Execution pausiert
  2. Stile werden berechnet
  3. Layout für das gesamte Dokument (oder Subtree) neu berechnet
  4. Wert wird zurückgegeben

In einer Schleife über 1.000 Elemente verursacht das 1.000 Layout-Neuberechnungen. Kosten: ca. 94 ms (6 verlorene Frames bei 60 fps).

Virtuelles Scrollen: Das klassische Performance-Problem

Virtuelle Scroller (wie react-window, tanstack-virtual) müssen die Höhe jedes Elements kennen. Feste Höhen sind trivial, aber variable Textinhalte? Nightmare.

Meist werden Off-Screen-Elemente gerendert, gemessen und dann positioniert – das untergräbt den Sinn des virtuellen Scrollings und kostet Performance. Pretext.js eliminiert diesen Workaround: Du berechnest die Höhe exakt, bevor ein DOM-Knoten existiert.

Benchmarks

Pretext.js liefert:

Ansatz 1.000 Textblöcke 500 Textblöcke
DOM (getBoundingClientRect) ~94ms (6 Frames) ~47ms
Pretext.js (layout()) ~2ms ~0,09ms
Geschwindigkeitsunterschied ~47x schneller ~500x schneller

Gerade bei vielen Elementen ist der Unterschied dramatisch.

Wie funktioniert Pretext.js intern?

Die Bibliothek arbeitet in drei Phasen:

1. Textsegmentierung

prepare() normalisiert den Text, behandelt Leerzeichen, setzt Zeilenumbruchregeln (Unicode UAX #14) um und segmentiert in umbrechbare Einheiten.

Unterstützte Fälle:

  • CJK-Zeichen: Zeichenbasierte Umbrüche
  • Arabisch, Hebräisch: Bidirektionalität
  • Thailändisch: Wörterbuchbasierte Segmentierung
  • Hindi/Devanagari: Ligaturen
  • Emoji: Korrekte ZWJ-/Sequenz-Behandlung
  • Weiche Trennstriche: ­ wird erkannt

2. Canvas-Messung

Jedes Segment wird über Canvas measureText() gemessen (kein 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

Die Ergebnisse werden gecached. Gleicher Text + gleiche Schriftart = kein erneuter Browserzugriff.

3. Reines arithmetisches Layout

layout() nutzt die gecachten Breiten und Containerbreite, bricht Zeilen mit einem Greedy-Algorithmus um. Kein DOM, keine Canvas-Operation – nur Addition und Vergleich.

const handle = prepare(longArticleText, '16px "Inter"');
const mobile = layout(handle, 375, 24);   // { height: 2400, lineCount: 100 }
const tablet = layout(handle, 768, 24);   // { height: 1200, lineCount: 50 }
const desktop = layout(handle, 1200, 24); // { height: 720, lineCount: 30 }
Enter fullscreen mode Exit fullscreen mode

Ein Handle, beliebig viele Layouts für verschiedene Breiten – ideal für Responsive Designs.

Praktische Anwendungsfälle

1. Virtuelles Scrollen mit variabler Texthöhe

So integrierst du Pretext.js in einen virtuellen 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 für Padding
  });
}

// 10.000 Items in ~4ms messen
const heights = computeHeights(chatMessages, 600);
Enter fullscreen mode Exit fullscreen mode

Keine Off-Screen-Elemente, keine Schätzungen, keine Sprünge beim Scrollen.

2. KI-Chat-Oberflächen (Streaming)

Bei AI-Chats wird Text Token für Token gestreamt – jede Änderung kann Zeilenumbrüche verursachen. Mit Pretext.js misst du die Höhe nach jedem Token ohne DOM-Kontakt:

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. Datengrids mit Textspalten

Für automatische Spaltenbreitenmessung – kein DOM nötig:

function computeColumnWidth(values: string[], font: string, padding: number) {
  let maxWidth = 0;
  for (const value of values) {
    const handle = prepare(value, font);
    const { height } = layout(handle, Infinity, 20);
    // TODO: maxWidth anhand gemessener Breite setzen
  }
  return maxWidth + padding;
}
Enter fullscreen mode Exit fullscreen mode

(Füge hier die Berechnung für die Breite gemäß deiner Anforderungen hinzu.)

4. Mehrsprachige Feeds

Gemischte Sprachen? Gleiche API:

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

Teste dein Textlayout mit Apidog

Wenn du API-gestützte, textlastige UIs baust, reicht gutes Layout nicht – die API-Antworten müssen auch stimmen.

Apidog Beispiel

Apidog macht es einfach, Streaming-API-Antworten zu simulieren und zu testen, wie Pretext.js damit umgeht. Du kannst verschiedene Textlängen, Sprachen und Unicode-Grenzfälle testen und prüfen, ob dein Virtueller Scroller korrekt arbeitet.

Für KI-Chat-Produkte bietet Apidog:

  • Simulation von Streaming-Antworten (Token für Token)
  • Tests mit mehrsprachigen Payloads
  • Schemas validieren (richtige Textfelder)
  • Automatisierte Testsuiten für Text-Rendering-Grenzfälle

Eine Textlayout-Bibliothek ist nur so gut wie die Daten, die sie bekommt. Schlechte API-Antworten = schlechtes Layout, egal wie schnell die Engine ist.

Bekannte Einschränkungen und Kritikpunkte

Rendering-Genauigkeit: Grenzfälle

  • Ungewöhnliche Fonts/Kerning-Paare
  • Mixed Font Sizes innerhalb eines Blocks
  • Subpixel-Unterschiede (Canvas vs. DOM)
  • Browserspezifische Eigenheiten

Für virtuelles Scrollen meist unwichtig, für Print-Layouts relevant.

Canvas-Messung ist nicht kostenlos

prepare() nutzt die Canvas-Engine. Wenn du tausende neue Handles pro Frame erstellst, wird das ggf. zum Bottleneck. Nutze Caching!

Keine CSS-Eigenschaften

Nicht unterstützt:

  • letter-spacing
  • word-spacing
  • text-indent
  • text-transform
  • font-feature-settings
  • font-variant

Wenn dein Layout auf diesen Eigenschaften basiert, musst du nachjustieren.

Kein Rendering

Pretext.js misst nur – für das eigentliche Rendering brauchst du weiterhin DOM/Canvas/SVG.

Pretext.js vs. klassische Ansätze

Funktion Pretext.js DOM-Messung Geschätzte Höhen
Geschwindigkeit (1K) ~2ms ~94ms ~0ms
Genauigkeit Hoch (Canvas) Perfekt Niedrig
DOM-Abhängigkeit Nur prepare() Vollständig Keine
Reflow Null 1 pro Messung Null
Mehrsprachigkeit Volle Unicode Browser Schlecht
CSS-Eigenschaften Nur Font Vollständig Keine
Speicherverbrauch Segment-Cache DOM-Knoten Minimal
Responsive Layouts 1 Handle, viele Layouts Pro Breite neu Pro Breite neu

Für höchste Genauigkeit & CSS-Support: DOM. Für Geschwindigkeit und Virtualisierung: Pretext.js.

Erste Schritte

Installation

npm install @chenglou/pretext
# oder
pnpm add @chenglou/pretext
# oder
bun add @chenglou/pretext
Enter fullscreen mode Exit fullscreen mode

Grundlegende Verwendung

import { prepare, layout } from '@chenglou/pretext';

const handle = prepare(
  'Pretext.js computes text layout without touching the DOM.',
  '16px "Inter"'
);

const result = layout(handle, 600, 24);
console.log(result.height);    // z.B. 48 (2 Zeilen x 24px)
console.log(result.lineCount); // z.B. 2
Enter fullscreen mode Exit fullscreen mode

Integration mit 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

So erhältst du einen performanten virtuellen Chat mit exakt berechneten Elementhöhen vor dem Rendern.

Interaktiver Playground

Teste dein Layout live unter pretextjs.dev/playground.

Wann solltest du Pretext.js nicht verwenden?

  • Statische Seiten: CSS reicht.
  • Pixelgenaue Drucklayouts: DOM bleibt Referenz.
  • CSS-lastige Textstile: Nicht unterstützt.
  • Serverseitiges Rendering: Canvas-API ist clientseitig.
  • Kleine, statische Listen: Optimierung lohnt nicht.

FAQ

Ist Pretext.js produktionsreif?

Frisch, aber erprobt. Große Testsuite, stabile API – aber: Fixiere die Version und prüfe mit deinen Schriften/Inhalten.

Funktioniert Pretext.js mit React, Vue, Svelte?

Ja, Framework-agnostisch. Zwei Funktionen, überall nutzbar.

Wie verhält es sich mit Webfonts?

prepare() misst mit der geladenen Schriftart. Achte darauf, dass Fonts geladen sind (document.fonts.ready).

Canvas- oder SVG-Rendering?

Ja, Layout-Ergebnisse sind rendering-agnostisch.

RTL-Support?

Ja, mit voller Bidirektionalität.

Wie groß ist das Bundle?

~15KB minimiert, keine externen Abhängigkeiten.

Wie genau ist das Ergebnis?

Meist innerhalb 1-2 Pixel des DOM. CSS-Eigenschaften wie letter-spacing werden nicht berücksichtigt.

Stilisierten Text messen?

Ein prepare() pro Fontspec. Für gemischte Stile: Text selbst segmentieren und mehrere Handles erzeugen.

Fazit

Pretext.js löst endlich performante, genaue Textmessung ohne DOM-Reflow. Für virtuelle Listen, Chat-UIs und Datagrids ersetzt die Library komplexe Workarounds durch zwei einfache Funktionsaufrufe.

Sie ist kein Allheilmittel: CSS-Eigenschaften fehlen, Subpixel-Deltas gibt es, SSR ist noch nicht möglich. Aber für performante Virtualisierung ist aktuell nichts schneller oder flexibler.

Bereit, performante, textlastige UIs zu bauen? Teste deine API-Endpunkte mit Apidog und integriere Pretext.js in deine Rendering-Pipeline.

Top comments (0)