Pretext
Pure JS/TS · DOM リフロー不要 · 多言語テキスト計測 & レイアウトライブラリ
何をするライブラリか
Pretext は、getBoundingClientRect や offsetHeight などの DOM 計測を一切使わずに、テキストの折り返し後の高さと行数を計算する Pure JS/TypeScript ライブラリ。DOM 計測はブラウザの同期レイアウトリフローを引き起こし、500 テキストブロックを個別に計測すると 1 フレームで 30ms 以上失う原因になる。Pretext はブラウザの font エンジンを正解として canvas の measureText で字幅をキャッシュし、以降の高さ計算を純粋な算術演算で行う。
19ms
prepare() / 500テキスト
0.09ms
layout() / 同バッチ
CJK / Bidi
多言語 + 絵文字対応
DOM に触れないことで得られるメリット: 仮想化・サーバーサイドレンダリング・WebGL テキスト計測など、通常 DOM が使えない文脈でもレイアウト計算が可能になる。
Prepare → Layout の分離
処理を「一度だけ走る重い前処理(prepare)」と「リサイズのたびに走る軽い計算(layout)」の二段階に分けることで、ホットパスのコストを最小化する。
prepare()
テキスト正規化 · Intl.Segmenter でセグメント分割 · canvas measureText で字幅計測 · 結果をキャッシュ
→
PreparedText
不透明なキャッシュオブジェクト(再利用可能)
→
layout()
キャッシュ済み幅 + maxWidth + lineHeight で純粋算術 → { height, lineCount }
重要: 同じテキスト・フォント設定に対して
prepare() を何度も呼ばないこと。リサイズ時は layout() だけ再実行するのが正しいパターン。
i18n 処理の概要
- CJK:
Intl.Segmenterにより文字単位で改行処理(日本語・中国語・韓国語) - Bidi (双方向テキスト): Arabic/Hebrew など RTL 混在テキストのメタデータを付与
- 絵文字補正: Chrome/Firefox は macOS で小さいフォントサイズのとき canvas が絵文字を DOM より広く計測する。Pretext は DOM との差分を 1 回だけ測定してキャッシュし、自動補正する
- 禁則処理: 句読点が行頭/行末に来ないよう kinsoku ルールを適用
- overflow-wrap: 長い単語はグラフィーム境界で折り返す
DOM を触らずに段落の高さを取得する
最もシンプルな使い方。prepare() で前処理してから layout() で高さを計算する。
typescript
import { prepare, layout } from '@chenglou/pretext'
// ① テキストを事前計測(フォントが確定したら一度だけ実行)
const prepared = prepare('AGI 春天到了. بدأت الرحلة 🚀', '16px Inter')
// ② リサイズのたびに高さを計算(DOM アクセスなし)
const { height, lineCount } = layout(prepared, textWidth, 20)
// textarea スタイルの改行・タブを維持したい場合
const prepared2 = prepare(textareaValue, '16px Inter', { whiteSpace: 'pre-wrap' })
const { height: h2 } = layout(prepared2, textareaWidth, 20)
API 詳細
| 関数 | 引数 | 戻り値 | 説明 |
|---|---|---|---|
prepare() |
text: stringfont: stringoptions?: { whiteSpace? } |
PreparedText |
正規化・セグメント・計測を一括処理。font は CSS font 短縮形と同じ形式(例: "16px Inter") |
layout() |
prepared: PreparedTextmaxWidth: numberlineHeight: number |
{ height, lineCount } |
キャッシュ済み幅から純粋算術で計算。DOM アクセスなし。~0.0002ms/テキスト |
Canvas / SVG / WebGL への描画に
prepareWithSegments() を使うと、各行のテキスト文字列・幅・カーソル情報を取得できる。Canvas / SVG / サーバーサイド描画など、DOM を使えない環境での実際の描画に使う。
layoutWithLines — 固定幅で全行を取得
typescript
import { prepareWithSegments, layoutWithLines } from '@chenglou/pretext'
const prepared = prepareWithSegments('AGI 春天到了. بدأت الرحلة 🚀', '18px "Helvetica Neue"')
const { lines } = layoutWithLines(prepared, 320, 26) // 320px 幅、26px 行高
for (let i = 0; i < lines.length; i++) {
ctx.fillText(lines[i].text, 0, i * 26)
}
walkLineRanges — 幅だけ知りたいとき
typescript
// 最も幅の広い行を調べる("shrink-wrap" レイアウト)
let maxW = 0
walkLineRanges(prepared, 320, line => {
if (line.width > maxW) maxW = line.width
})
// maxW = テキストがちょうど収まる最小コンテナ幅
layoutNextLine — 行ごとに幅が変わる場合
typescript
// 画像の横にテキストを流し込む(行によって幅が異なる)
let cursor = { segmentIndex: 0, graphemeIndex: 0 }
let y = 0
while (true) {
const width = y < image.bottom ? columnWidth - image.width : columnWidth
const line = layoutNextLine(prepared, cursor, width)
if (line === null) break
ctx.fillText(line.text, 0, y)
cursor = line.end
y += 26
}
API 詳細 — ユースケース 2
| 関数 / 型 | 概要 |
|---|---|
prepareWithSegments(text, font, opts?) |
prepare() と同じ前処理を行い、手動レイアウト用の豊富な構造を返す |
layoutWithLines(prepared, maxWidth, lineHeight) |
全行の { text, width, start, end } 配列を返す。固定幅向け |
walkLineRanges(prepared, maxWidth, onLine) |
行テキストを生成せず幅とカーソルだけ返す。バイナリサーチ等の投機的探索に最適 |
layoutNextLine(prepared, start, maxWidth) |
イテレータ型 API。1 行ずつ取得し、行ごとに異なる幅を指定できる |
LayoutLine |
{ text: string; width: number; start: LayoutCursor; end: LayoutCursor } |
LayoutCursor |
{ segmentIndex: number; graphemeIndex: number } |
clearCache() |
内部キャッシュを全クリア。フォントを多種切り替えるアプリでメモリ管理に使用 |
setLocale(locale?) |
ロケールを変更(呼び出し時に clearCache() も実行)。既存の prepared 値には影響しない |
デフォルトの CSS 想定
Pretext はフル機能のフォントエンジンではなく、以下の CSS 設定を前提としている。
| CSS プロパティ | 想定値 | 備考 |
|---|---|---|
white-space | normal or pre-wrap | pre-wrap はオプションで有効化。スペース・タブ・改行を保持 |
word-break | normal | — |
overflow-wrap | break-word | 非常に狭い幅ではグラフィーム境界で単語内折り返し可能 |
line-break | auto | — |
macOS の
system-ui に注意: canvas は DOM と異なる optical variant に解決されるため計測精度が落ちる。"Inter" や "Helvetica" など名前付きフォントを使うこと。
公式・関連
- GitHub: chenglou/pretext — ソースコード
- chenglou.me/pretext — ライブデモ
- somnai-dreams.github.io/pretext-demos — 追加デモ
- chenglou/text-layout — 先行研究(Sebastian Marbage 設計)
- npm: @chenglou/pretext — パッケージ