Published 2026.04.06

Pretext:绕过 DOM Reflow,重新定义浏览器文本测量与排版

一篇关于 Pretext 的深度文章,聚焦浏览器文本测量、排版性能以及 userland 文本引擎的设计思路。

2 分钟阅读 Capoo

当一个 15KB 的库能把文本测量速度提升 300 倍时,也许我们该重新审视浏览器的文本排版机制了。

从一个生产环境的痛点说起

如果你做过任何涉及动态文本高度计算的前端工作——自适应文本框、虚拟列表中的可变行高、流式输出的 AI 聊天界面——你一定对 getBoundingClientRect()offsetHeight 不陌生。这些 API 简单好用,但它们有一个致命的代价:触发浏览器的 layout reflow。

在简单场景下,一次 reflow 的开销可以忽略不计。但当你的页面需要频繁、密集地测量文本(比如 AI 对话界面中逐 token 流式渲染时),数百次 reflow 叠加起来就会导致明显的 UI 卡顿。这不是理论上的担忧——这是 Midjourney 团队在生产中遇到的真实问题。

2026 年 3 月底,Cheng Lou 发布了 Pretext,一个纯 JavaScript/TypeScript 的多行文本测量与排版库。它的核心思路出奇地直接:完全绕过 DOM,利用 Canvas 的 measureText() API 直接访问浏览器的字体引擎,在用户态(userland)完成所有文本排版计算。

发布 48 小时内,Pretext 在 GitHub 上收获了超过 14,000 颗星。

作者背景

了解 Cheng Lou 的背景有助于理解这个项目为什么值得关注。他曾是 Facebook 的 React 核心团队成员,创建了拥有超过 21,000 星的 react-motion 动画库,主导了 ReasonML(后来演变为 ReScript)的开发,并参与了 Messenger 前端的构建。目前他在 Midjourney 工作,那里大约 5 名工程师用 Bun 和原生 React 支撑着数百万用户的 UI。

Pretext 正是从这样的大规模生产场景中诞生的。

核心架构:prepare + layout 的两阶段模型

Pretext 的设计哲学可以用一句话概括:把昂贵的测量和廉价的计算彻底分离。

第一阶段:prepare()——做一次性的重活。它接收文本、字体和配置选项,完成空白符规范化、文本分段、胶合规则(glue rules)应用,以及通过 Canvas measureText() 测量每个分段的宽度,最后返回一个不透明的句柄(opaque handle)。

第二阶段:layout()——纯算术运算。传入准备好的句柄、最大宽度和行高,它就能立刻返回文本的排版高度。这一步完全不接触 DOM,不触发任何浏览器 layout。

这种分离意味着什么?一次 prepare() 之后,你可以用不同的 maxWidth 反复调用 layout() 而几乎零成本。在需要响应容器宽度变化的场景(比如窗口 resize、响应式布局)中,这是一个巨大的优势。

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

const prepared = prepare("你的多行文本内容...", "16px/1.5 sans-serif", {
  whiteSpace: "normal",
  wordBreak: "normal",
});

// 纯算术,可以反复调用
const height1 = layout(prepared, 300, 24); // 容器宽 300px
const height2 = layout(prepared, 500, 24); // 容器宽 500px

性能数据

根据多方报道,Pretext 在文本测量操作上实现了约 300-600 倍的性能提升。具体数字因场景而异,但量级是一致的:传统 DOM 测量一次操作约 30ms 并伴随多次 reflow,而 Pretext 只需约 0.05ms 且零 layout pass。

对于 AI 聊天界面这样高频测量的场景,这个差距直接决定了用户体验是流畅还是卡顿。

API 全貌

Pretext 的 API 设计按使用场景分层,从简单到高级逐步暴露更多控制力。

基础层用于最常见的需求——获取文本高度。prepare() + layout() 两个函数就够了,适合虚拟列表、自适应文本框等场景。

中间层提供行级别的控制。prepareWithSegments() 返回更丰富的结构数据,layoutWithLines() 给出每一行的详细信息,walkLineRanges() 提供迭代器风格的行遍历,measureLineStats() 返回行数和宽度统计。这些 API 适合需要自定义渲染的场景。

高级层面向完全自定义排版的需求。layoutNextLine() 支持可变宽度布局(比如文字环绕图片),materializeLineRange() 将计算结果转化为完整的行数据。这些 API 让你可以将文本渲染到 Canvas、SVG、WebGL,甚至服务端。

此外,Pretext 还提供了一个独立模块 @chenglou/pretext/rich-inline,用于处理混合字体的富文本内联内容,包括 mentions 和 chips 等组件,同时保持 white-space: normal 的语义。

多语言支持与准确性挑战

文本排版远不只是英文分词换行这么简单。Pretext 在多语言支持上下了大量功夫,覆盖了 CJK(中日韩)、阿拉伯语、韩文(Hangul)等多种文字系统。项目的 RESEARCH.md 文件详细记录了团队在准确性方面遇到的挑战和发现。

一个有趣的发现是字体解析不一致问题:在 macOS 上,Canvas 和 DOM 对 system-ui 字体的解析在特定字号(10-12px、14px、26px)下会得到不同的 SF Pro 变体。Pretext 的解决方案是建议使用具名字体而非 system-ui

另一个挑战是宽度累积误差。逐词测量宽度再求和,在长段落中会积累微小的相邻误差。团队发现,与其在运行时做复杂的校正模型,不如在预处理阶段做好语义归并——比如将标点和前后文合并测量、保留尾随空格——效果更好。

项目维护了跨 Chrome、Safari 和 Firefox 的准确性测试快照,包含多语言语料库,涵盖中文(宋体 SC vs 苹方 SC 的差异)、日文(假名迭代标记处理)、缅甸文(目前仍有未解决的边界情况)以及混合应用文本(URL、软连字符、emoji ZWJ 序列)等场景。

一个独特的能力:Shrink-wrap 文本测量

Pretext 解决了一个 Web 平台长期缺失的能力:找到能容纳多行文本的最紧凑容器宽度(shrink-wrap width),而不需要额外的约束条件。

这个功能在 CSS 中一直很难实现。通常你需要创建一个 DOM 元素、设置样式、插入文本、读取尺寸——整个过程既昂贵又笨拙。而 Pretext 通过 walkLineRanges() 配合二分搜索,可以在纯计算中找到这个最优宽度。类似地,你还可以实现「均衡文本排版」(balanced text layout)——让多行文本的每行宽度尽量接近。

AI 驱动的开发方式

Pretext 的开发过程本身也值得一提。Cheng Lou 采用了 AI 辅助的迭代方法:反复让 Claude 和 Codex 等模型将 TypeScript 排版逻辑与实际浏览器渲染进行对照——测试语料包括《了不起的盖茨比》全文和多样化的多语言数据集——以此达到像素级的准确度。

这种开发方式与 Pretext 自身的定位形成了有趣的呼应:它既是为 AI 应用界面而生的工具,也是用 AI 辅助构建的产品。

设计哲学:把能力还给用户态

Pretext 背后有一个更大的技术主张。在项目的 thoughts.md 中,Cheng Lou 写道他认为 CSS 规范中相当大一部分复杂性,如果用户态拥有更好的文本控制能力,本可以避免。

这个观点的逻辑链是这样的:CSS 的表达力在增长,但性能在下降,而标准委员会很难同时解决这两个问题。浏览器规范的巨大复杂性阻止了新浏览器引擎的竞争。解决方案是把更多能力下放到用户态——这理论上是所有浏览器厂商都支持的方向。

Pretext 就是这种哲学的一个具体实践:与其等待浏览器优化文本测量的性能,不如在用户态重新实现它。

局限性

Pretext 并不试图成为一个完整的字体渲染引擎。它使用 Canvas 宽度进行换行决策,而非精确的字形定位数据。这意味着它非常适合排版计算,但不适合需要字符级精确定位的复杂文字场景。

目前支持的 CSS 文本属性包括 white-space(normal、pre-wrap)、word-break(normal、keep-all)、overflow-wrapline-break。软连字符(soft hyphen)和 Tab 也得到了支持。

缅甸文的某些边界情况(引号跟随类)仍未完全解决,这标志着当前架构的精度天花板。

快速上手

npm install @chenglou/pretext

最简单的用例——测量文本高度:

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

const p = prepare("Hello, world! This is a long text...", "16px Arial");
const height = layout(p, 200, 24); // maxWidth=200, lineHeight=24

需要行级控制时:

import { prepareWithSegments, layoutWithLines } from "@chenglou/pretext";

const p = prepareWithSegments(text, font);
const lines = layoutWithLines(p, maxWidth, lineHeight);
// lines 包含每行的详细信息,可用于 Canvas/SVG 渲染

项目提供了在线 Demo 可以直接体验:chenglou.me/pretext

写在最后

Pretext 的意义不仅在于它解决了一个具体的性能问题。它更深层的价值在于提出了一种思路:对于浏览器做得不够好的事情,也许我们不必永远等待标准和引擎的改进,而是可以在用户态找到更好的答案。

对于正在构建 AI 应用界面、富文本编辑器、或任何需要高性能文本测量的前端开发者来说,Pretext 值得认真评估。一个 15KB、零依赖、支持全语言的库,能把你最昂贵的 DOM 操作之一变成一次纯算术调用——这样的工具不常有。


参考链接

  • GitHub 仓库:https://github.com/chenglou/pretext
  • 在线 Demo:https://chenglou.me/pretext
  • npm 包:https://www.npmjs.com/package/@chenglou/pretext