Pretext.js:テキストレイアウトを500倍高速化する15KBの軽量ライブラリ

Ashley Innocent

Ashley Innocent

31 3月 2026

Pretext.js:テキストレイアウトを500倍高速化する15KBの軽量ライブラリ

Apidog エンタープライズ

オンプレミスデプロイ

SSO & RBAC

SOC 2 準拠

Apidog Enterpriseを見る

要約

Pretext.jsは、DOM操作の代わりに純粋な算術演算によって複数行のテキストを測定し、配置するゼロ依存のTypeScriptライブラリです。これは、強制同期リフローを排除し、getBoundingClientRect()よりも約500倍速いテキスト測定を実現し、地球上の主要なあらゆる書字システムをサポートします。仮想スクローラー、チャットUI、データグリッドを構築する場合、このライブラリはブラウザが30年間無視してきた問題を解決します。

はじめに

JavaScriptがgetBoundingClientRect()を呼び出すかoffsetHeightを読み取るたびに、ブラウザはすべての処理を停止します。保留中のスタイル変更をフラッシュし、レイアウトを再計算し、完全なレンダリングパスを強制します。これは強制同期リフローと呼ばれ、ブラウザが実行できる最も高価な単一の操作です。

これを仮想リスト内の1,000個のチャットバブル、またはデータグリッド内の10,000行に掛けてみてください。結果はどうでしょう?フレームのドロップ、カクつき、そしてアプリが壊れていると考えるユーザーです。

💡
Apidogのチームは、API駆動型のフロントエンドを構築する際にこの苦痛をよく知っています。レイアウトエンジンがすべての段階であなたと戦うとき、レスポンスデータを動的なUIにストリーミングしながら、すべてをスムーズに保つのは絶え間ない戦いです。
ボタン

react-motion(GitHubで21,700以上のスター)の開発者であり、Meta社のReactおよびReasonMLのコアコントリビューターであるCheng Louは、この問題を解決するためにPretext.jsを構築しました。このライブラリは2026年3月にリリースされ、数日でGitHubで14,000以上のスターを獲得し、その年最大のHacker Newsスレッドの1つを巻き起こしました。

この記事では、Pretext.jsが何をするのか、その内部構造、いつ使用すべきか、そしてその欠点について詳しく説明します。このライブラリがあなたの技術スタックに属するかどうかが分かるようになるでしょう。

Pretext.jsとは?

Pretext.jsは、純粋なJavaScript/TypeScriptのテキストレイアウトエンジンです。`getBoundingClientRect()`や`offsetHeight`を使わず、リフローやスラッシングなしに、完全に算術演算によって複数行のテキストを測定し、配置します。

その核となるアイデアはシンプルです。ブラウザに「このテキストの高さはどれくらいですか?」と尋ねる(これはブラウザにまずそれをレンダリングさせることになる)代わりに、Pretext.jsはCanvas APIのフォントメトリクスを使用して算術的に答えを計算します。

これがAPIの全貌です。

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

// ステップ1: テキストを準備 (一度のみ、キャッシュ可能)
const handle = prepare('Hello, pretext.js', '16px "Inter"');

// ステップ2: 任意の幅でレイアウト (純粋な算術演算、マイクロ秒単位)
const { height, lineCount } = layout(handle, 400, 24);

以上です。2つの関数があります。1つはテキストセグメントを測定してキャッシュし、もう1つはレイアウトを計算するために算術演算を行います。prepare()の呼び出しは、ブラウザに触れる唯一の操作(Canvas measureText()を介して)です。その後、layout()は純粋な数学です。

APIを多用するアプリケーションにとってこれが重要な理由

AIアシスタント、リアルタイムダッシュボード、共同編集ツールなど、ストリーミングAPIレスポンスを消費するアプリを構築している場合、受信するテキストをレンダリングする前にその高さを知る必要があります。それがなければ、仮想スクローラーは途切れ、チャットUIは飛び跳ね、ユーザーはそれに気づくでしょう。

Pretext.jsは、ミリ秒単位ではなくマイクロ秒単位でその高さを提供します。この違いは急速に積み重なります。

Pretext.jsが解決する問題

このライブラリが存在する理由を理解するには、JavaScriptがレイアウトプロパティを読み取るときに何が起こるかを理解する必要があります。

強制同期リフローの解説

このコードを書くと:

const elements = document.querySelectorAll('.text-block');
elements.forEach(el => {
  const height = el.getBoundingClientRect().height; // REFLOW!
  // 配置のために高さを利用...
});

getBoundingClientRect()の呼び出しは、ブラウザに以下のことを強制します:

  1. JavaScriptの実行を一時停止する
  2. 保留中のスタイル変更をすべてフラッシュする
  3. ドキュメント全体(またはサブツリー)のレイアウトを再計算する
  4. 計算された値を返す

これは「レイアウトスラッシング」と呼ばれます。1,000個の要素を測定するループでは、ブラウザは1,000回の完全なレイアウト再計算を実行します。そのコストは?およそ94ミリ秒で、これは60fpsで6フレームのドロップを意味します。

仮想スクロールの問題

仮想スクロールライブラリ(react-windowtanstack-virtualなど)は、スクロール位置を計算するためにすべてのアイテムの高さを知る必要があります。高さが固定のアイテムの場合は簡単ですが、高さが可変のテキストコンテンツの場合は悪夢です。

標準的な回避策は、アイテムを画面外でレンダリングし、測定し、それから配置することです。これは機能しますが、仮想スクロールの目的を損ないます。レンダリングを避けようとしているDOMノードをレンダリングしていることになります。

一部のライブラリは高さを推定し、レンダリング後に修正するため、目に見えるジャンプが発生します。また、開発者に固定の高さを指定させるため、表示できる内容が制限されます。

Pretext.jsは、この種の回避策をすべて排除します。DOMノードが存在する前に正確なテキストの高さを計算できます。

実測値

Pretext.jsは、彼らのサイトでベンチマーク結果を公開しています。

アプローチ テキストブロック1,000個 テキストブロック500個
DOM (getBoundingClientRect) 約94ms (6フレームドロップ) 約47ms
Pretext.js (layout()) 約2ms 約0.09ms
速度差 約47倍速い 約500倍速い

DOM測定の呼び出しごとのオーバーヘッドは一定である一方、Pretextの算術コストは線形に増加するため、より小さいバッチの場合、速度の改善はより劇的になります。

Pretext.jsの内部構造

このライブラリは3つの異なるフェーズで動作します。これらを理解することは、ライブラリの利用方法を最適化するのに役立ちます。

フェーズ1: テキストのセグメンテーション

prepare()を呼び出すと、Pretext.jsは最初に入力テキストを正規化します。空白文字を処理し、Unicodeの改行ルール(UAX #14)を適用し、テキストを分割可能な単位にセグメント化します。

ここで多言語サポートが機能します。セグメンテーションエンジンは以下を正しく処理します:

フェーズ2: Canvasでの測定

セグメンテーションの後、Pretext.jsは各セグメントをCanvas measureText() APIに通します。これがライブラリが行う唯一のブラウザ呼び出しであり、Canvasのテキスト測定はレイアウトのリフローをトリガーしないため、高速です。

// 内部処理: Pretextがテキストを測定する方法
const ctx = offscreenCanvas.getContext('2d');
ctx.font = '16px "Inter"';
const metrics = ctx.measureText('Hello'); // リフローなし!
const width = metrics.width; // グリフの送り幅

測定値はセグメントとフォントの組み合わせごとにキャッシュされます。同じテキストとフォントでprepare()を2回呼び出すと、2回目の呼び出しはキャッシュされたデータを再利用します。

フェーズ3: 純粋な算術レイアウト

layout()関数は、キャッシュされたセグメント幅とコンテナ幅を受け取り、貪欲法アルゴリズムを使用して改行を計算します。

  1. 合計がコンテナ幅を超えるまでセグメント幅を合計する
  2. 新しい行に改行する
  3. すべてのセグメントが配置されるまで繰り返す
  4. 行数にline-heightを掛けて合計高さを得る

DOMなし。Canvasなし。純粋な足し算と比較です。

これがlayout()が非常に高速である理由です。定規と電卓を使って紙に書くのと同じ計算をしています。

再利用可能なハンドルのパターン

Pretext.jsの最高の設計決定の1つは、prepare()が再利用可能なハンドルを返すことです。1回のprepare()呼び出しは、すべてのコンテナ幅で機能します。

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 }

このパターンは、レスポンシブデザインの計算に最適です。一度測定すれば、あらゆる幅で瞬時にレイアウトできます。

実用的なユースケース

1. 可変高さテキストの仮想スクロール

これが主要なユースケースです。以下に、Pretext.jsを仮想スクローラーと統合する方法を示します。

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
  });
}

// 10,000アイテムを約4ミリ秒で測定
const heights = computeHeights(chatMessages, 600);

画面外レンダリングなし。高さの推定なし。アイテムがスクロール表示されたときの目に見えるジャンプなし。

2. AIチャットインターフェース

AIアシスタントは、トークンごとにレスポンスをストリーミングします。新しいトークンごとに、行数が変わり、その下にあるすべてがシフトします。従来のDOM測定では、トークンの更新ごとにリフローがトリガーされます。

Pretext.jsを使用すると、DOMに触れることなく、チャンクが到着するたびに高さを再計算できます。

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);
  
  // DOM測定なしで仮想スクローラーの位置を更新
  scroller.updateItemHeight(messageId, height + padding);
});

3. テキストカラムのあるデータグリッド

スプレッドシートスタイルのアプリには、カラムの自動サイズ調整が必要です。DOMを介して数千のセル値を測定するのはコストがかかります。Pretext.jsはこれを高速化します。

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);
    // カラムのサイズ調整のためにハンドルの内部幅追跡を使用
    maxWidth = Math.max(maxWidth, /* 計算された幅 */);
  }
  return maxWidth + padding;
}

4. 多言語コンテンツフィード

混在スクリプトのコンテンツ(中国語の投稿にアラビア語の返信、その後に韓国語のコメントが続くなど)を含むソーシャルメディアフィードは、各スクリプトに異なる改行ルールがあるため、仮想化が非常に困難です。

Pretext.jsは、同じ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' },
];

// 同じAPIで、すべてのスクリプトで正しい結果
posts.forEach(post => {
  const handle = prepare(post.text, '16px system-ui');
  const { height } = layout(handle, 400, 24);
});

Apidogでテキストレイアウトをテストする

APIをバックエンドとするテキスト量の多いUIを構築する場合、レイアウトを正しくすることは戦いの半分に過ぎません。テキストコンポーネントに供給されるAPIレスポンスが、正しいデータ、正しい形式、正しい速度で配信されることを検証する必要もあります。

Apidogはこれを簡単にします。ストリーミングAPIレスポンスをモックして、Pretext.jsの統合がプログレッシブテキスト読み込みをどのように処理するかをテストできます。異なるテキスト長、言語、Unicodeのエッジケースでテストシナリオを設定し、デプロイ前に仮想スクローラーが正しく動作することを確認できます。

AIチャット製品を構築するチームにとって、ApidogのAPIテスト環境は以下のことを可能にします:

これは重要です。なぜなら、テキストレイアウトライブラリは、そこに入力されるデータの品質に依存するからです。測定エンジンがどれほど高速に動作しても、不適切なAPIレスポンスは不適切なレイアウトを生み出します。

ボタン

既知の制限と批判

Pretext.jsは完璧ではありません。Hacker Newsのスレッドでは、導入前に知っておくべきいくつかの正当な懸念が表面化しました。

レンダリング精度のエッジケース

複数のユーザーが、SafariとChromeのデモでテキストがバウンディングボックスを超えて表示されると報告しました。ライブラリの算術計算は、特定のシナリオでブラウザのネイティブレイアウトから乖離する可能性があります:

これらのエッジケースは、仮想スクロール(数ピクセルの誤差は目に見えない)ではあまり重要ではなく、ピクセルパーフェクトな組版においてより重要になります。

Canvasでの測定は無料ではない

prepare()の呼び出しは、ブラウザのCanvasテキストエンジンに依然としてアクセスします。フレームごとに数千ものユニークなprepare()ハンドルを作成するアプリケーションでは、これがボトルネックになる可能性があります。解決策はキャッシングとバッチ処理ですが、ライブラリはどちらも強制しません。

CSSプロパティのサポートなし

Pretext.jsは、フォント指定に基づいて生のテキストを測定します。レイアウトに影響を与えるCSSプロパティは考慮しません:

テキストのスタイル設定がこれらのCSSプロパティに依存している場合、計算された高さはブラウザがレンダリングするものと一致しません。これらを手動で考慮するか、不一致を受け入れる必要があります。

DOMレンダリングを置き換えるものではない

Pretext.jsはテキストの高さがどれくらいになるかを教えてくれます。テキストをレンダリングするわけではありません。テキストを表示するには、依然としてDOMノード(またはCanvas/SVGレンダリング)が必要です。このライブラリの価値は測定フェーズにあり、レンダリングフェーズにはありません。

Pretext.js vs. 従来のアプローチ

特徴 Pretext.js DOM測定 高さの推定
速度 (1,000アイテム) 約2ms 約94ms 約0ms (測定なし)
精度 高い (Canvasベース) 完璧 (真実) 低い (ヒューリスティック)
DOM依存性 prepare()後はなし 完全 なし
リフローのトリガー ゼロ 測定ごとに1回 ゼロ
多言語対応 完全なUnicodeサポート 完全 (ブラウザネイティブ) 貧弱 (ハードコードされた比率)
CSSプロパティのサポート 制限的 (フォントのみ) 完全 なし
メモリオーバーヘッド キャッシュされたセグメント DOMノード 最小限
レスポンシブレイアウト prepare()1回、layout()複数回 幅ごとに再測定 幅ごとに再推定

適切な選択はあなたの制約によります。ピクセルパーフェクトな精度とCSSプロパティのサポートが必要な場合、DOM測定が依然として真実です。数千のアイテムで速度が必要で、わずかなサブピクセルの違いを許容できるのであれば、Pretext.jsが圧倒的に優れています。

はじめに

インストール

npm install @chenglou/pretext
# or
pnpm add @chenglou/pretext
# or
bun add @chenglou/pretext

基本的な使い方

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);    // 例: 48 (2行 x 24px)
console.log(result.lineCount); // 例: 2

Reactとの統合

import { prepare, layout } from '@chenglou/pretext';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useMemo, useRef } => 'react';

function VirtualChat({ messages }: { messages: string[] }) {
  const parentRef = useRef(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; // パディング
    });
  }, [messages]);

  const virtualizer = useVirtualizer({
    count: messages.length,
    getScrollElement: () => parentRef.current,
    estimateSize: (index) => heights[index],
  });

  return (
    
      
        {virtualizer.getVirtualItems().map(virtualRow => (
          
            {messages[virtualRow.index]}
          
        ))}
      
    
  );
}

これにより、メッセージがDOMにレンダリングされる前に正確なアイテムの高さが計算された仮想チャットが得られます。推定なし、修正ジャンプなし、リフローなし。

インタラクティブプレイグラウンド

Pretext.jsのウェブサイトには、pretextjs.dev/playgroundでインタラクティブなプレイグラウンドが含まれており、テキストを貼り付け、フォントを選択し、コンテナの幅を調整し、リアルタイムでレイアウト計算を確認できます。これは、統合前に動作を検証する最も速い方法です。

Pretext.jsを使用すべきでない場合

Pretext.jsは、すべてのテキスト測定問題に適したツールではありません:

よくある質問

Pretext.jsはプロダクションレディですか?

このライブラリは2026年3月にリリースされ、数日でGitHubで14,000以上のスターを獲得しました。作者のCheng Louは、数百万人のユーザーにサービスを提供するプロダクションシステムであるMidjourneyのフロントエンドを運営しています。このライブラリのテストスイートは、数十の言語とエッジケースをカバーしています。とは言え、これは新しいリリースです。バージョンを固定し、特定のフォントとコンテンツに対してテストしてください。

Pretext.jsはReact、Vue、Svelteで動作しますか?

はい。Pretext.jsはフレームワークに依存しません。これは2つの関数を持つ純粋なTypeScriptライブラリです。Reactフック、Vueコンポーザブル、Svelteストア、または純粋なJavaScriptのどこでも、テキスト測定が必要な場所でprepare()layout()を呼び出すことができます。

Pretext.jsはWebフォントをどのように扱いますか?

prepare()関数は、呼び出し時にブラウザがロードしている任意のフォントを使用してテキストを測定します。Webフォントがまだロードされていない場合、測定はフォールバックフォントを使用し、不正確な結果を生成します。prepare()を呼び出す前に、フォントがロードされていることを確認してください。Font Loading API (document.fonts.ready) を使用して検証できます。

Pretext.jsをCanvasまたはSVGレンダリングに使用できますか?

はい。このライブラリは、レンダリングターゲットに依存しないテキストレイアウトを計算します。計算された高さと改行を使用して、Canvas 2D、WebGL、SVG、またはDOMでテキストを配置できます。Pretext.jsのウェブサイトでは、これらのすべてのレンダリングターゲットの例が示されています。

RTL(右から左)言語をサポートしていますか?

はい。Pretext.jsは、アラビア語、ヘブライ語、およびその他のRTLスクリプトを、適切な双方向テキストサポートで処理します。また、混在方向のテキスト(例:埋め込み英語単語を含むアラビア語テキスト)も正しく処理します。

バンドルサイズはどれくらいですか?

15KBのミニファイ済みでゼロ依存性です。ポリフィルは不要です。このライブラリは、標準のブラウザAPI(利用可能な場合はCanvas measureText()Intl.Segmenter)のみを使用します。

DOM測定と比較してどのくらい正確ですか?

ほとんどのテキストコンテンツにおいて、Pretext.jsはDOMレイアウトと1〜2ピクセルの範囲で一致します。精度は使用するフォントとCSSプロパティに依存します。letter-spacingword-spacingのようなプロパティは考慮されないため、これらを使用する場合はより大きな差異を予想してください。数ピクセルの誤差が目に見えない仮想スクロールの場合、精度は十分に満足できるものです。

Pretext.jsはスタイル付きテキスト(太字、斜体、混合サイズ)を測定できますか?

prepare()呼び出しは単一のフォント指定を受け取ります。スタイルが混在するテキスト(通常のテキスト内の太字の単語など)の場合、テキスト自体をセグメント化し、各スタイルランに対して個別のハンドルを作成する必要があります。これは既知の制限であり、将来のバージョンで対処される可能性があります。

結論

Pretext.jsは、ブラウザが30年間無視してきた問題、つまりDOMリフローなしでの高速かつ正確なテキスト測定を解決します。仮想スクローラー、チャットUI、データグリッド、または数千のテキストブロックを測定する必要があるあらゆるインターフェースを構築する開発者にとって、このライブラリは2つの関数呼び出しで回避策のカテゴリ全体を置き換えます。

このライブラリは万能薬ではありません。フォント指定以外のCSSテキストプロパティをサポートせず、わずかなサブピクセルの精度差があり、まだサーバーサイドでは動作しません。しかし、そのターゲットとするユースケース、すなわち仮想化されたリストのテキスト高さを事前計算することに関しては、他に匹敵するものはありません。

より高速なテキスト量の多いUIを構築する準備はできましたか? まずApidogでAPIエンドポイントをテストしてデータレイヤーが堅牢であることを確認し、その後Pretext.jsをレンダリングパイプラインに組み込みましょう。

ボタン

ApidogでAPIデザイン中心のアプローチを取る

APIの開発と利用をよりシンプルなことにする方法を発見できる