DEV Community

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

Posted on • Originally published at apidog.com

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

要約

Pretext.jsは、DOM操作を使わず純粋な算術演算のみで複数行テキストを高速に測定・配置できるゼロ依存のTypeScriptライブラリです。強制同期リフローを排除し、getBoundingClientRect()より最大500倍速く、世界中の主要な書字システムをサポートします。仮想スクローラーやチャットUI、大規模なデータグリッド構築時に、長年ブラウザが抱えてきたパフォーマンス課題をシンプルに解決します。

Apidog を今すぐ試す

はじめに

JavaScriptでgetBoundingClientRect()offsetHeightを読むと、ブラウザは強制的にレイアウトを再計算し、アプリ全体のパフォーマンスが著しく低下します。これが「強制同期リフロー」です。これを1,000個のチャットバブルや10,000行のデータグリッドで繰り返すと、フレーム落ちやカクつきが発生し、ユーザー体験を損ないます。

💡Apidogのチームは、API駆動フロントエンド開発時のこの苦労を熟知しています。レスポンスデータを動的UIにストリーミングしながらスムーズさを保つのは至難の業です。

react-motionの開発者でReactやReasonMLのコアコントリビューターCheng Louがこの課題に取り組み、Pretext.jsを開発(2026年3月リリース)。GitHubで爆発的にスターを獲得し、注目を集めています。

この記事では、Pretext.jsの仕組み・使い方・ユースケース・制限・他手法との比較・導入方法まで実装目線で詳しく解説します。


Pretext.jsとは?

Pretext.jsは、getBoundingClientRect()offsetHeightを用いず、Canvas APIのフォントメトリクスと算術演算のみでマルチラインテキストのサイズを瞬時に計算するTypeScriptライブラリです。

pretext.js image

基本API例

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

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

// 2: 任意の幅でレイアウト計算(超高速)
const { height, lineCount } = layout(handle, 400, 24);
Enter fullscreen mode Exit fullscreen mode
  • prepare() … テキストとフォント情報から測定用ハンドル生成(内部でCanvasを1回だけ使用)
  • layout() … 算術演算のみで高さ・行数計算(DOM・Canvasアクセスなし)

なぜAPI多用アプリでは重要か

AIチャットやリアルタイムダッシュボード、コラボ編集ツール等で、ストリーミングAPIレスポンスを描画する前に高さを把握する必要があります。Pretext.jsなら1,000要素でもマイクロ秒単位で一括測定可能です。


Pretext.jsが解決する問題

強制同期リフローとは

const elements = document.querySelectorAll('.text-block');
elements.forEach(el => {
  const height = el.getBoundingClientRect().height; // REFLOW!
  // 高さを使って配置...
});
Enter fullscreen mode Exit fullscreen mode

このようなコードは、各呼び出しごとに

  1. JS実行停止
  2. スタイル変更のフラッシュ
  3. レイアウト再計算
  4. 値取得 を発生させます。1000回ループで約94msかかり、60fpsなら6フレーム落ちに直結。

仮想スクロールの課題

react-windowtanstack-virtualなど仮想リストは、各アイテムの高さが必要。高さ可変のテキストでは、画面外レンダリングや推定値→補正ジャンプ...と複雑な実装になりがち。Pretext.jsなら、DOMノード生成前に正確な高さを取得できます。

ベンチマーク

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

Pretext.jsの内部構造

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

prepare()時、テキストをUnicodeルールに則って分割。各言語・スクリプト特有の改行や分割ポイントを完全考慮。

  • CJK文字: 1文字ごとに改行可
  • アラビア・ヘブライ: 双方向対応
  • タイ語: スペースなし単語も辞書分割
  • 絵文字/合字/ソフトハイフンも対応

フェーズ2: Canvasでの測定

各セグメントをCanvasのmeasureText()で幅測定。リフローなし・高速。

const ctx = offscreenCanvas.getContext('2d');
ctx.font = '16px "Inter"';
const metrics = ctx.measureText('Hello');
const width = metrics.width;
Enter fullscreen mode Exit fullscreen mode

同一テキスト・フォントはキャッシュで再利用。

フェーズ3: 算術でレイアウト

layout()はキャッシュ済み幅とcontainer幅で貪欲法改行。lineCount × lineHeightで高さを算出。すべて純粋な演算なので超高速。

ハンドル再利用パターン

1回のprepare()で、異なる幅へのlayout()を何度でも実行可能。レスポンシブ対応やマルチデバイスに最適。

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

実用的なユースケース

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

仮想リストの高さ計算に最適。例:

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

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 }; // パディング分
  });
}

// 10,000個でも数msで完了
const heights = computeHeights(chatMessages, 600);
Enter fullscreen mode Exit fullscreen mode

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

ストリーミング受信ごとに高さを即時算出:

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. テキストカラムのあるデータグリッド

各セル値の自然幅や高さを高速測定して、カラム自動調整が可能。

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

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

複数スクリプト混在でもワンライナーで対応可能。

posts.forEach(post => {
  const handle = prepare(post.text, '16px system-ui');
  const { height } = layout(handle, 400, 24);
});
Enter fullscreen mode Exit fullscreen mode

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

API駆動UI構築時は、テキストレイアウトだけでなくAPIレスポンスの形式・速度・多言語対応も検証が不可欠です。

apidog image

Apidogは、ストリーミングAPIレスポンスのモックや多様なテキスト長・言語・エッジケースの自動テストに対応し、Pretext.jsと組み合わせて仮想スクロールやチャットUIの動作検証を効率化します。

AIチャット製品開発チームには特に有用です:

  • チャンク化テキストでストリーミングAPIをモック
  • 多言語ペイロードで自動テスト
  • レスポンススキーマ検証
  • テキストレンダリングのエッジケース網羅

既知の制限と批判

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

  • Safari/Chrome間のわずかなバウンディング差
  • 異常なカーニング・混在フォントサイズ
  • サブピクセル誤差
  • ブラウザ独自のテキスト整形の違い

仮想スクロール用途では問題になりにくいですが、厳密なピクセル精度が必要な用途では注意。

Canvas測定は無料でない

prepare()多発・大量生成はCanvas呼び出しのボトルネックとなる場合あり。キャッシュやバッチ処理で回避。

CSSプロパティ未サポート

letter-spacing等のCSSプロパティは計算に含まれません。スタイル依存が強い場合は注意。

DOMレンダリングの代替ではない

あくまで「測定」用。実際のテキスト描画には引き続きDOM/Canvas/SVG等が必要です。


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

特徴 Pretext.js DOM測定 高さ推定
速度 (1,000アイテム) 約2ms 約94ms 約0ms
精度 高 (Canvas) 完全 低 (ヒューリスティック)
DOM依存性 prepare()後なし 完全 なし
リフロー発生 ゼロ 毎回 ゼロ
多言語対応 完全 完全 貧弱
CSSプロパティ 制限あり 完全 なし
メモリ セグメントキャッシュ DOMノード 最小
レスポンシブ 1回prepare()+複数layout() 幅ごとに再測定 幅ごとに再推定

はじめに

インストール

npm install @chenglou/pretext
# または
pnpm add @chenglou/pretext
# または
bun add @chenglou/pretext
Enter fullscreen mode Exit fullscreen mode

基本的な使い方

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

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(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 (
    <div ref={parentRef}>
      {virtualizer.getVirtualItems().map(virtualRow => (
        <div key={virtualRow.index}>
          {messages[virtualRow.index]}
        </div>
      ))}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

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

Pretext.js公式サイトのplaygroundで、フォント・テキスト・幅を変えてリアルタイムでレイアウト動作検証可能です。


Pretext.jsを使うべきでないケース

  • 静的な既知コンテンツ:CSSで十分
  • ピクセルパーフェクトな印刷:サブピクセル誤差不可の場合はDOM測定推奨
  • 重いCSSテキスト装飾letter-spacingなど依存時は非推奨
  • SSR(サーバーサイドレンダリング):現状Node.js未対応
  • 小規模リスト(50件以下):最適化メリット小

よくある質問

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

2026年3月リリース・数日で14,000スター獲得。作者は大規模プロダクションフロントエンド運用経験あり。多言語エッジケースもテスト済みですが、新しめなのでバージョン固定・自社環境で十分検証推奨。

React/Vue/Svelte対応?

完全にフレームワーク非依存。TypeScriptの2関数のみで、どこでも利用可能。

Webフォントの扱いは?

prepare()呼び出し時点でWebフォントがロード済みか要注意。未ロードならフォールバックフォントで測定されるので、document.fonts.ready等で事前に確認してください。

Canvas/SVG描画にも使える?

はい。得られる高さ・改行情報はDOM/Canvas/WebGL/SVG等あらゆる描画ターゲットで使えます。

RTL言語(右から左)対応?

アラビア語・ヘブライ語・混在方向テキストも双方向サポートで正しく測定します。

バンドルサイズは?

15KB(minified、ゼロ依存)。ポリフィル不要。Canvas measureTextIntl.Segmenterのみ利用。

DOM測定と精度比較は?

ほとんどのフォント・コンテンツで1〜2ピクセル差。letter-spacing等のCSS未対応部分のみ差異あり。仮想スクロール等では十分な精度。

スタイル付きテキスト対応?

1回のprepare()は1つのフォント指定のみ。混在スタイルの場合はテキストを分割し、各スタイルごとに個別に測定する必要があります。


結論

Pretext.jsは、DOMリフローなしでの高速・多言語対応テキスト測定を実装2関数で実現できます。仮想リストやチャットUI、大量テキストのデータグリッドなど、従来の回避策を不要にする新定番です。

万能ではありませんが、仮想化リストの高さ計算用途では現状No.1。まずApidogでAPIデータレイヤーをテストし、Pretext.jsをUIレイヤーに組み込みましょう。

Top comments (0)