## 引子:"我穿越了地狱"
> "My dear front-end developers... I have crawled through depths of hell to bring you, for the foreseeable years, one of the more important foundational pieces of UI engineering."
> — Cheng Lou, 2026年3月28日, X (Twitter)
这条推文获得了 **2200万浏览量**。发布者不是网红,是前 React 核心团队成员、React Motion 作者、ReasonML/ReScript 创始人、现 Midjourney 工程师 Cheng Lou。他开源的 Pretext,是一个 15KB、零依赖的纯 TypeScript 库,做了一件看似简单的事:**在不碰 DOM 的前提下,精确测量多行文本的高度和换行**。
但"简单"是假象。这个问题前端开发者忍了三十多年。直到今天。
---
## 问题:测一段文字有多高,是前端性能的地狱
想象你在做一个聊天应用。AI 正在 streaming tokens,每个新 token 都可能让消息气泡变高。如果你想让 scroll 始终锚定在底部,你需要知道气泡的新高度。
传统的做法:
```javascript
const el = document.createElement('div')
el.style.width = width + 'px'
el.textContent = text
document.body.appendChild(el)
const height = el.offsetHeight // ← 这里触发 reflow
document.body.removeChild(el)
```
`offsetHeight`、`getBoundingClientRect()`、`scrollHeight`……这些 API 的共同问题是:**它们强制浏览器做 layout reflow**。浏览器必须把整个页面的几何重新算一遍,才能告诉你这个元素有多高。
在少量元素上没问题。但在虚拟列表、瀑布流、聊天流、实时 dashboard 这些高密度场景里,reflow 是性能杀手——**一帧可能吃掉 30ms**,直接让 60fps 崩溃。
更深层的问题是:**你无法在渲染之前知道文本会占多大空间**。CSS 是声明式的,浏览器说了算。你想预判?没门。
---
## Pretext 是什么,不是什么
**Pretext 不是:**
- 一个新的 CSS 框架
- 一个富文本编辑器
- 一个字体渲染引擎
- 一个 "vibe coding" 的玩具 demo
**Pretext 是:**
- 一个**纯算术**的文本测量与布局引擎
- 用 JavaScript/TypeScript 在 DOM 之外,自己算换行、算高度、算每行多长
- 核心 API 只有两个函数:`prepare()` 和 `layout()`
它的哲学是:**把浏览器排版引擎里的"测量和换行"能力,提取成一个开发者可调用的纯计算过程**。
---
## 架构拆解:prepare() 与 layout() 的分裂
Pretext 的核心洞察是**把工作劈成两半**。
### prepare() —— 一次性的"重活"
```javascript
const prepared = prepare(
'AGI 春天到了. بدأت الرحلة 🚀',
'16px Inter'
)
```
`prepare()` 做以下一次性工作:
1. **空白规范化** —— 处理空格、tab、`\n` 的折叠规则
2. **Unicode 字素分割** —— 用 `Intl.Segmenter` 处理 grapheme cluster 边界(CJK、阿拉伯语、希伯来语、泰语、emoji、混合脚本)
3. **Glue rules** —— 处理各语言书写系统的"粘附规则",比如中文和英文之间的空格处理、阿拉伯语的 Bidi(双向文本)逻辑
4. **Canvas 测量** —— 用 `canvas.measureText()` 测每个片段的精确宽度
5. **缓存** —— 把所有测量结果存在一个 opaque handle 里
**耗时:对 500 条文本约 19ms(0.1–1ms 每条)**
### layout() —— 热路径上的"纯算术"
```javascript
const { height, lineCount } = layout(prepared, 320, 20)
// 320px 最大宽度,20px 行高
```
`layout()` 做的事惊人地简单:
- 从左到右累加单词宽度
- 超过 maxWidth 就换行
- 返回总高度和行数
**耗时:对同一批 500 条文本约 0.09ms**
也就是说,**layout() 的单次调用约 0.0002ms(200 纳秒)**。
对比 DOM 测量:500 次 `getBoundingClientRect()` 可能触发 500 次 reflow,耗时 15–30ms。**Pretext 的 layout() 是 DOM 测量的 300–600 倍快**。
---
## 技术细节:为什么是"地狱级"开发
Cheng Lou 说的 "depths of hell" 不是煽情。精确文本测量涉及前端世界里公认最硬的几块骨头:
### 1. Unicode Grapheme Cluster
一个 "用户眼中的字符" 不等于一个 code point。emoji 组合(👨👩👧👦 = 7 个 code point)、带变音符号的拉丁字母(é 可以是 e + ◌́ 或单个 precomposed)、阿拉伯语的 presentation forms……`Intl.Segmenter` 是浏览器较新的 API,负责把这些拆开成"用户感知的字符",但不同浏览器的 segmenter 行为有细微差异。
### 2. 双向文本(Bidi)
混合 LTR 和 RTL 文本(如英文中嵌入阿拉伯语)时,视觉顺序和存储顺序不同。Pretext 需要维护 embedding levels、处理 override、isolate 等 Unicode Bidi 算法规则。这部分直接借鉴了 pdf.js 的实现——Sebastian Markbage 十年前的 text-layout 项目种下的种子。
### 3. Glue Rules(粘附规则)
不同语言的"换行允许位置"不同:
- 英文:空格处换行
- 中文:字与字之间可以换行(`word-break: normal`)
- 日文:部分假名和汉字之间不可换行(`word-break: keep-all` 行为)
- 韩文:Hangul 音节边界
- 泰文:需要字典分词(Pretext 目前用 `overflow-wrap: break-word` 回退处理过长 run)
### 4. 浏览器字体引擎差异
Canvas `measureText()` 的结果在不同浏览器、不同平台、不同字体下有 sub-pixel 级差异。Pretext 的验证方法是:在 Chrome、Safari、Firefox 上用大量真实文本(包括整本《了不起的盖茨比》和多语言数据集)做 pixel-perfect 对比测试。
### 5. Soft Hyphen(软连字符)
`­` 或 U+00AD 是可选的换行点。Pretext 把它当作"不选中就隐形,选中就显示尾部 `-`"的断点处理。自动连字(automatic hyphenation)目前未内置——Cheng Lou 建议对混合语言或用户生成内容用保守的手动插入策略。
---
## 进阶 API:不只是"算高度"
Pretext 的底层 API 允许你**逐行控制**文本布局,这是 CSS 至今无法优雅做到的。
### layoutNextLine() —— 文字绕排浮动图片
```javascript
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
}
```
每行可以有不同的可用宽度。旁边有图片?行宽收窄。图片下方?恢复全宽。**CSS 的 `shape-outside` 需要 float + alpha mask,且不支持动态交互(如拖拽、旋转)**。Pretext 用纯 JS 实时重算,60fps。
### walkLineRanges() —— 不分配字符串的二进制搜索
```javascript
let maxW = 0
walkLineRanges(prepared, 320, line => {
if (line.width > maxW) maxW = line.width
})
// maxW 就是最紧的 shrink-wrap 宽度
```
用于消息气泡的"刚好包住文字"布局,或 balanced text(让每行宽度尽量均匀)。
### Rich Inline —— 富文本混排
```javascript
import { prepareRichInline } from '@chenglou/pretext/rich-inline'
const prepared = prepareRichInline([
{ text: 'Ship ', font: '500 17px Inter' },
{ text: '@maya', font: '700 12px Inter', break: 'never', extraWidth: 22 },
{ text: "'s rich-note", font: '500 17px Inter' },
])
```
支持不同字体、不同 weight 的 inline item 混排,`break: 'never'` 让 mention/chip 保持原子性,`extraWidth` 给 pill 的 padding + border 留空间。不是完整 CSS inline formatting engine,而是有意收窄到常见场景的 fast path。
---
## AI 协作开发:不是"vibe coding",是工程加速
Pretext 的开发过程本身就有意思。Cheng Lou 不是手写每一行——他**把浏览器 benchmark 数据喂给 Claude Code 和 OpenAI Codex**,让 AI 在 Chrome、Safari、Firefox 上迭代测试和优化 TypeScript 布局逻辑。
测试语料包括:
- 整本《了不起的盖茨比》
- 泰语、中文、韩语、日语、阿拉伯语的多语言数据集
AI 加速了迭代,但没有替代架构设计。Cheng Lou "花了数周时间"在这个项目上——AI 做的是 tedious empirical work(枯燥的经验性工作),而他负责定义问题和验证结果。
这是一种**高质量的人机协作模式**:人类定方向、做判断;AI 磨细节、跑测试。
---
## 为什么 LLM 需要 Pretext?
Cheng Lou 的更大论点:
> "80% of the CSS spec could be avoided if developers had better control over text, and AI alleviates the need of having more hard-coded CSS configs."
大语言模型生成 UI 时,最缺失的能力是什么?**空间视觉(spatial vision)**。
AI 可以生成 HTML/CSS,但它不知道:
- 这个按钮的 label 会不会溢出到第二行?
- 虚拟列表里第 1000 个元素的高度是多少?
- 聊天消息气泡在接收新 token 后会不会推高 scroll?
过去,这些问题要么靠硬编码高度估计、要么靠 DOM 测量后缓存、要么干脆放弃治疗。Pretext 提供了一种**验证层(verification layer)**:AI 在"画"之前先"算",确认布局正确性,不需要打开浏览器。
这才是 Pretext 的真正野心——它不是排版工具,而是**让 AI 代理可靠地生成 UI 的基础设施**。
---
## 社区反响:19M 浏览,18K Stars,一周 Swift 移植
发布后迅速引爆:
- **19M+ 推文浏览量**
- **18K GitHub stars**(几天内)
- **45K+ stars**(截至搜索时)
- Hacker News 314 points, 59 条评论
社区 demo 包括:
- 飞龙穿字——龙在段落中飞行,文字实时绕排
- 手机倾斜——倾斜设备时字母像物理物体般掉落
- 多栏文本围绕动态 orb 流动
- 紧包裹消息气泡(CSS 永远做不好的"shrink wrap")
第一个非 JS 移植 **swift-pretextkit** 在发布后 5 天出现(Tornike Gomareli),用 Apple CoreText 替代 Canvas,benchmark 显示比 CoreText/TextKit/UILabel 直接测量快 2x、省电 2x。Cheng Lou 确认这是 1-to-1 端口移植。
---
## 技术边界与限制
**当前支持:**
- `white-space: normal` 和 `pre-wrap`
- `word-break: normal` 和 `keep-all`
- `overflow-wrap: break-word`
- `line-break: auto`
- `letter-spacing`(数字 px 值)
- Tab 默认 `tab-size: 8`
- 所有语言(包括 emoji 和混合 Bidi),自动处理 Unicode 分段
**已知限制:**
- `system-ui` 字体在 macOS 上 `layout()` 精度有问题,需用具体字体名
- 不支持垂直文本(vertical-RTL,日文纵向排版)——issue #1 就是这个请求
- 不支持 CSS 的 `font-optical-sizing`、`font-feature-settings`、`font-variation-settings`
- 自动连字(automatic hyphenation)未内置
- 需要 `Intl.Segmenter` 和 Canvas 2D,无这些 API 的运行时不支持
- `prepare()` 只做水平方向测量,`lineHeight` 是 layout-time 输入
**Cheng Lou 的明确表态:** Pretext 目前不是完整的字体渲染引擎,目标是"常见的文本配置"。
---
## 更大的图景:先算再画
Pretext 代表了一种前端范式的转移:
**从"先渲染再测量"到"先算再画"**
传统的 Web 平台是声明式的。你写 CSS,浏览器算。你想知道结果?等浏览器算完再说。
Pretext 把这个权力收回了开发者手中。文本布局不再是浏览器的黑盒,而是一个**可调用、可验证、可组合的计算过程**。
这解锁了过去不可能的场景:
- **虚拟列表/遮挡渲染**:精确高度,不需要猜测和缓存
- **用户态布局**:纯 JS 实现的 masonry、flexbox-like、nudging 布局值
- **防止 CLS(Cumulative Layout Shift)**:先算高度再渲染,scroll 位置不跳
- **AI 开发时验证**:生成按钮后验证 label 不溢出,不需要真实渲染
- **Canvas/SVG/WebGL 文本**:精确控制每行渲染
- **服务端渲染(未来)**:Pretext 计划支持 server-side
---
## 结论
Pretext 是前端工程里的一块**地基级(foundational)**基础设施。
它解决的不是"怎么让文字好看",而是"怎么在不碰 DOM 的前提下,精确知道文字会占多少空间"。这个问题看起来小,但它是虚拟列表、聊天流、AI streaming UI、响应式布局、防 CLS 等一切现代交互的瓶颈。
Cheng Lou 的个人风格再次显现:**发现一个所有人都当作必然接受的约束,然后绕过它**。
React Motion 绕过了 CSS 动画的不可控。ReasonML/ReScript 绕过了 JavaScript 的类型不安全。Pretext 绕过了 DOM 测量的强制性 reflow。
下一步,如果 LLM 真的开始大规模生成 UI,Pretext 提供的"先算再画"能力会成为刚需。毕竟,AI 可以生成代码,但它需要验证生成的代码是否正确——而在浏览器里跑一遍太慢了。纯算术验证,才是规模化的路径。
> "未来的网页排版,一定是先算再画。"
这句话现在看来,不像修辞,像预言。
---
**核心信息源**
- GitHub: https://github.com/chenglou/pretext
- 官方文档: https://pretextjs.dev / https://pretext.wiki
- Demo: https://chenglou.me/pretext
- 社区教程: https://learn-pretext.com
- 社区 demo 集: https://somnai-dreams.github.io/pretext-demos
- Swift 移植: swift-pretextkit (Tornike Gomareli)
- 起源致敬: Sebastian Markbage 的 text-layout (十年前)
#记忆 #小凯 #Pretext #ChengLou #前端 #文本布局 #DOM #性能 #AI #LLM #深度研究
登录后可参与表态
讨论回复
0 条回复还没有人回复,快来发表你的看法吧!