Pretext 研究筆記¶
資料來源¶
| 項目 | 連結 |
|---|---|
| GitHub | chenglou/pretext |
| npm | @chenglou/pretext |
| Live Demos | chenglou.me/pretext |
| 社群 Demos | somnai-dreams.github.io/pretext-demos |
| 作者 Twitter | @_chenglou |
| Josh Kale 推文介紹 | X |
| react-motion(同作者) | chenglou/react-motion(21.7K stars) |
專案概述¶
Pretext 是 Cheng Lou 開發的零依賴 TypeScript 函式庫,解決一個存在 30 年的網頁核心問題:DOM 文字測量造成的 layout reflow 瓶頸。
核心思路:把文字排版拆成兩步——prepare() 一次性用 Canvas 測量所有 segment 寬度並快取,layout() 在熱路徑上只做純算術。結果是 layout() 的成本降到 每個文字區塊 0.0002ms,比傳統 DOM interleaved 測量快 480x(Chrome)到 1,240x(Safari)。
截至 2026-03-29(發布僅 3 天),已累積 4,599 stars、178 forks。這是 Cheng Lou 繼 react-motion(21.7K stars)之後又一個引爆社群的專案。
作者背景¶
Cheng Lou 是前端/程式語言社群的知名人物:
- Meta 時期:React 團隊核心成員,主導 ReasonML 開發,參與 ReScript、Messenger.com
- 代表作:react-motion — 基於物理彈簧的 React 動畫庫(21.7K stars)
- 著名演講:
- "On the Spectrum of Abstraction"(React Europe 2016)— 用統一框架理解抽象層級
- "Taming the Meta Language"(React Conf 2017)— 語言 vs 後設語言的流動性
- 現職:Midjourney 工程師
核心問題¶
當 UI 需要知道「這段文字有多高?」時(虛擬化、動畫、Layout Shift 防止等),傳統做法是呼叫 getBoundingClientRect() 或讀取 offsetHeight,但每次讀取都會觸發瀏覽器的同步 layout reflow。多個元件讀寫交錯時,瀏覽器反覆重新排版整份文件。
Benchmark(500 個文字區塊)¶
| 方法 | Chrome | Safari |
|---|---|---|
Pretext layout() |
0.09ms | 0.12ms |
| DOM batch 測量 | 4.05ms | 87.00ms |
| DOM interleaved 測量 | 43.50ms | 149.00ms |
layout() 比 DOM interleaved 快約 480x(Chrome) 到 1,240x(Safari)。
技術架構¶
兩階段模型¶
階段 1: prepare(text, font) ← 一次性,用 Canvas 測量
正規化空白 → Intl.Segmenter 分詞 → 標點合併 → CJK 拆分
→ Canvas measureText() → 快取 segment 寬度 → Emoji 修正
階段 2: layout(prepared, maxWidth, lineHeight) ← 熱路徑,純算術
零 DOM 讀取 | 零 Canvas 呼叫 | 零字串操作 | 零記憶體配置
→ { height, lineCount }
階段 1 細節(prepare)¶
- 正規化空白:匹配 CSS
white-space: normal行為 Intl.Segmenter分詞:正確處理 CJK 逐字斷行、泰文、阿拉伯文等- 標點合併:將 "better." 當作一個測量單位(匹配 CSS 行為)
- CJK 字元拆分:每字元可獨立斷行
- Canvas
measureText()測量:按 (segment, font) 快取 - 長單字 grapheme 預測量:支援
overflow-wrap: break-word - Emoji 修正:自動偵測並修正 Chrome/Firefox 在 canvas 上測量 emoji 偏大的問題
- 可選 Bidi metadata:供自定義渲染器使用
內部 Segment 模型¶
區分 8 種 break kind:
| Kind | 說明 |
|---|---|
text |
一般文字 |
space |
空白 |
preserved-space |
保留空白 |
tab |
Tab |
glue |
NBSP/Word Joiner(不可斷行) |
zero-width-break |
零寬斷行點 |
soft-hyphen |
軟連字號 |
hard-break |
強制換行 |
原始碼結構¶
| 檔案 | 職責 |
|---|---|
src/layout.ts |
核心公開 API |
src/analysis.ts |
文字正規化、分詞、膠合規則 |
src/measurement.ts |
Canvas 測量、快取、emoji 修正、瀏覽器 profile |
src/line-break.ts |
內部斷行核心 |
src/bidi.ts |
簡化 bidi metadata(源自 pdf.js,via Sebastian Markbage) |
src/test-data.ts |
共享測試語料庫 |
src/layout.test.ts |
不變性測試 |
大小:~1.7 kB min+gzip / ~8 kB raw,零依賴。
API 設計¶
Use Case 1:純高度預測(最常用)¶
import { prepare, layout } from '@chenglou/pretext'
// 一次性:測量所有 segment 寬度
const prepared = prepare('AGI 春天到了. بدأت الرحلة', '16px Inter')
// 熱路徑:純算術,每次 resize 都可呼叫
const { height, lineCount } = layout(prepared, 320, 20)
Use Case 2:手動逐行排版¶
// 完整行資訊
prepareWithSegments(text, font, options?) -> PreparedTextWithSegments
layoutWithLines(prepared, maxWidth, lineHeight) -> { lines: LayoutLine[] }
// 串流式:不建構字串,只回傳寬度/cursor
walkLineRanges(prepared, maxWidth, onLine) -> number
// 逐行迭代:每行可不同寬度(繞障礙物)
layoutNextLine(prepared, start, maxWidth) -> LayoutLine | null
輔助 API¶
i18n 深度支援¶
這是 Pretext 與其他文字測量庫的最大差異。在 corpora/ 目錄中包含 17+ 語言的長文本 canary:
| 語言 | 語料 | 精確度 |
|---|---|---|
| 日文 | 羅生門、蜘蛛の糸 | anchor 精確,Chrome 有小幅正向漂移 |
| 韓文 | 운수 좋은 날 | 完全精確 |
| 中文 | 祝福、故鄉 | Safari 精確,Chrome 有字型敏感性 |
| 泰文 | Nithan Vetal | 精確 |
| 高棉文 | Prachum Reuang Preng | 精確 |
| 緬甸文 | 兩篇故事 | 殘留 Chrome/Safari 分歧 |
| 烏爾都文 | چغد | Nastaliq/Naskh canary |
| 印地文 | Eidgah | 完全精確 |
| 阿拉伯文 | 兩篇長文 | 粗語料精確 |
| 希伯來文 | Masaot Binyamin | 完全精確 |
| Mixed app text | URL、emoji、時間範圍等 | 幾乎完全精確 |
瀏覽器精確度回歸門:7,680/7,680 測試全部通過(4 字型 x 8 尺寸 x 8 寬度 x 30 文字,跨 Chrome/Safari/Firefox)。
七個 Demo¶
| Demo | 說明 | 核心技術 |
|---|---|---|
| Accordion | 展開/收合區段 | 高度由 Pretext 計算而非 DOM |
| Bubbles (Shrinkwrap) | 訊息氣泡最緊寬度 | walkLineRanges() 做二分搜尋找最窄寬度 |
| Dynamic Layout | 固定高度編輯版面 | 障礙物感知標題路由 + 連續兩欄排版 |
| Variable Typographic ASCII | 粒子驅動 ASCII art | 比例字型 vs 等寬字型 |
| Editorial Engine | 動態球體、即時文字重排 | Pull quotes、多欄排版、零 DOM 測量 |
| Rich Text | 混合 inline 元素排版 | Code spans、links、chips |
| Masonry | 文字卡片遮蔽 | 高度預測來自 Pretext |
其中 Bubbles 展示了 CSS fit-content 無法做到的能力:用二分搜尋找到「保持相同行數的最窄寬度」。
實際應用場景¶
- 虛擬化/遮蔽:不需猜測或快取即可做正確的 row height 預測
- Masonry layout:純 JS 計算每張卡片高度
- JS-driven flexbox:微調 layout 值而不用 CSS hack
- 開發時驗證:AI 生成的 UI 可以在無瀏覽器環境下驗證按鈕文字是否溢出
- 防止 Layout Shift:新文字載入時重新錨定捲軸位置
- 雜誌/報紙式排版:文字繞障礙物流動、多欄排版
- 訊息氣泡 Shrinkwrap:找到最緊湊的寬度
技術研究發現(RESEARCH.md)¶
Cheng Lou 在開發過程中的關鍵發現:
1. system-ui 字型不安全¶
macOS 上 Canvas 和 DOM 在不同字體大小解析 system-ui 到不同字型變體(SF Pro Text vs SF Pro Display),造成測量結果不一致。
2. 逐詞加總的精確度策略¶
Canvas 內部足夠一致,加總 segment 寬度效果很好。但在完整段落上微小差異會累積成行邊界誤差。解法是語義前處理(標點合併、trailing space hanging),而非 runtime 修正。
3. Emoji Canvas/DOM 寬度差異¶
Chrome/Firefox 在小字體下 canvas 測量 emoji 偏寬。透過自動偵測和快取修正解決。
4. 被拒絕的方法¶
- DOM 隱藏元素測量 — 重新引入 reflow
- SVG
getComputedTextLength()— 效能不足 - Full-string verification in
layout()— 違反零 DOM 原則 - Pair correction models — 準確度不足
5. Sebastian Markbage 的遺產¶
原始 text-layout 原型的直覺是正確的:word/run 為快取單位、瀏覽器為測量基準、串流式貪婪斷行。Pretext 在此基礎上加入了工程紀律。
作者的願景(thoughts.md)¶
Cheng Lou 的觀點:
- 80% 的 CSS 規範在使用者空間有更好的文字控制時可以避免
- CSS 的便利性正在被侵蝕:更多 CSS 表達力帶來更差效能,而 AI 減少了對硬編碼 CSS 配置的需求
- 新的網頁瀏覽器引擎很難實現,因為規範太龐大;唯一的出路是將更多能力交給使用者空間
- 可驗證軟體的成本將趨近於零
這暗示 Pretext 不只是一個測量工具,而是 Cheng Lou 對「瀏覽器應該把什麼交給使用者空間」這個大問題的回答的第一步。
與替代方案比較¶
| 維度 | Pretext | 傳統 DOM 測量 | text-metrics 等 JS 庫 |
|---|---|---|---|
| 熱路徑成本 | ~0.0002ms 純算術 | 30ms+(interleaved reflow) | 仍需 DOM 或 canvas |
| Resize 效能 | 極低 | 每次觸發 reflow | 多數無快取策略 |
| i18n 支援 | 17+ 語言深度驗證 | 依賴瀏覽器 | 多數只支援拉丁文 |
| 手動排版 | 逐行變寬、繞障礙物 | 不可能 | 不可能 |
| Bidi | 有 metadata 支援 | 瀏覽器內建 | 多數無 |
| Soft Hyphen | 完整支援 | 瀏覽器內建 | 多數無 |
| 大小 | ~1.7 kB gzip | N/A | 各異 |
目前限制¶
- 目標 CSS 配置:
white-space: normal+word-break: normal+overflow-wrap: break-word+line-break: auto system-ui字型不安全- 尚未支援
break-all,keep-all,strict,loose,anywhere - 尚未支援 server-side 渲染(未來目標)
- 自動斷字(hyphenation)超出範圍
社群反應¶
"Every website you've ever used has a problem you've never noticed, and it's been here for 30 years... a Midjourney engineer just fixed it." — Josh Kale
"This is pure genius. Measuring and laying out text on the web without ever asking the browser." — Humi
三天內從 0 到 4,599 stars — Cheng Lou 的社群影響力與技術深度的雙重體現。