Pretext — DOMなしでテキスト高さを測定する純粋JSレイアウトライブラリ
Canvas measureText()で文字幅を取得し、行計算は純粋な算術演算のみ — レイアウトリフロー0
既存方式の問題:レイアウトリフロー
ブラウザでテキスト高さを測る従来の方法:
const rect = element.getBoundingClientRect();
const height = element.offsetHeight;
両方ともレイアウトリフローを強制する。ブラウザが要素のサイズを計算するには、レイアウトツリー全体を再計算する必要がある。仮想スクロールで500〜1,000項目の高さを事前計算する場面では、毎回リフローが発生してフレームが落ちる。
インストール
npm install @chenglou/pretext
import { prepare, layout } from '@chenglou/pretext'
// リッチテキスト(インラインスタイル混合)が必要な場合:
import { ... } from '@chenglou/pretext/rich-inline'
Pretextのアプローチ:Canvas measureText()
核心アイデア:
1. Canvas.measureText()でフォントエンジンから直接正確な文字幅を取得
2. 幅をキャッシュ
3. 改行計算は純粋な算術演算 — 文字幅を累積してコンテナ幅を超えたら改行
4. DOM一切アクセスなし、リフロー0
const prepared = prepare('AGI 春天到了. بدأت الرحلة 🚀', '16px Inter')
const { height, lineCount } = layout(prepared, textWidth, 20)
prepare()がCanvasで文字幅を測定する段階(重い、500テキストで約19ms)、layout()が算術演算で高さを計算する段階(非常に軽い、0.09ms)。prepareは一度だけ、layoutはコンテナサイズが変わるたび呼んでも0.09ms。
パフォーマンス
| 段階 | 時間(500テキスト) | 説明 |
|---|---|---|
prepare() |
約19ms | Canvas measureText() + キャッシュ(1回のみ) |
layout() |
約0.09ms | キャッシュ値での算術演算 |
| DOM方式 | 数百ms | getBoundingClientRect × 500 |
0.09msということは10,000回呼んでも1ms未満。仮想スクロールのリサイズ再計算も問題なし。
2つの利用パターン
1. 高さだけ必要: prepare() + layout() → 仮想リスト、オーバーフロー検出、レスポンシブレイアウト
2. 行レベルの制御: layoutWithLines() / walkLineRanges() / layoutNextLine() → Canvas/WebGLレンダリング、SVGテキスト折り返し、可変幅レイアウト(画像の横にテキストが流れるなど)、サーバーサイドレンダリング
多言語・複雑文字サポート
絵文字(Grapheme Cluster単位の正確な分割、🏳️🌈のような複合絵文字対応)、CJK(全角幅計算)、アラビア語/ヘブライ語(BiDi RTL)対応。
注意点(コミュニティの声)
Canvas measureText()の幅とDOMレンダリング時の幅は完全に一致しない場合がある:
フォントヒンティング: DOMとCanvasのテキストレンダラーがサブピクセルレベルで異なる
letter-spacing/word-spacing: CSSの字間がCanvasに反映されない
OpenType機能: リガチャ等が異なる処理
line-height: CSSのline-heightとCanvasのtextBaselineは異なる基準
結論:100%ピクセルパーフェクトではなく「十分に正確(good enough)」な測定。 仮想スクロールのような数ピクセルの差が問題にならない用途には完璧。CSSの複雑なタイポグラフィ属性に精密に合わせる場合は誤差が出うる。
制作者:chenglou
Reactの初期貢献者、Relay(FacebookのGraphQLクライアント)とReason(OCaml → JavaScriptコンパイラ)の中心開発者。現在Anthropicに在籍。コンパイラ技術者がテキストレイアウトという「パース+最適化」問題にアプローチした結果の高品質コード。
キーポイント
prepare(text, font): Canvas measureText()で各文字幅を測定+キャッシュ(1回のみ)
layout(prepared, width, lineHeight): キャッシュ幅で行計算 — 純粋算術、DOMアクセス0、リフロー0
パフォーマンス: prepare約19ms(500個)、layout約0.09ms(500個)— 10,000回呼んでも1ms未満
多言語対応: 絵文字(Grapheme Cluster)、CJK(全角)、アラビア語(RTL/BiDi)
注意: Canvas measureText() ≠ DOMレンダリング結果。letter-spacing、リガチャ等で誤差の可能性。「十分に正確」な測定