Pretext:值得关注的文本排版引擎

Pretext 是一个用 TypeScript 实现的用于多行文本精确测量和布局的引擎。不碰 DOM,不触发 reflow,却能完美匹配浏览器字体引擎在各种语言、emoji、混合文字方向下的真实表现。它刚刚发布,发展势头很猛。

我觉得这是过去十年里最值得关注的文本引擎之一。它不是小打小闹的优化,而是把文本这块一直卡着大家脖子的核心问题彻底解决掉了。

先说清楚它解决什么痛点。网页上但凡涉及动态文本,比如聊天消息、文章卡片、虚拟列表、自动换行输入框、响应式排版,你都得问浏览器"这个文本在多宽容器里到底占多少高"。过去唯一办法是扔进 DOM,读 getBoundingClientRect 或者 offsetHeight,然后立刻写回样式。这操作直接触发布局重排,浏览器要重新计算整个页面流式布局,尤其在长列表、频繁 resize、AI 生成内容实时流式输出的时候,性能直接雪崩。很多框架只好批量测量、缓存估算值、妥协精度,结果就是滚动卡顿、布局偏移、虚拟化高度猜不准、画布渲染对不上 CSS 样式。Pretext 把这一切全绕过去:一次 prepare 预计算,后面 layout 全是纯算术,毫秒级出结果,还跨浏览器一致。

它体积极小,支持所有主流语言,包括 CJK、阿拉伯文、希伯来文、emoji、混合 bidi,还特别处理了 Safari、Chrome、Firefox 各自的换行 quirks。作者 Cheng Lou 之前在 React、ReasonML、ReScript 这些项目里摸爬滚打,又在 Midjourney 干过视觉生成,对文本排版的需求理解得透彻。他说这是"爬过地狱"才搞出来的基础件,我完全相信。因为文本测量看似简单,实际是浏览器字体引擎 + Unicode 规范 + 各家实现差异的超级复杂组合,普通人根本遭不住。

核心 API 拆解与使用

库暴露两套 API,一套给"只需要高度"的场景,另一套给"手动控制每一行"的高级玩法。

先看简单那套,绝大多数业务场景够用:

typescript 复制代码
import { prepare, layout } from '@chenglou/pretext'

const prepared = prepare('AGI 春天到了. بدأت الرحلة 🚀', '16px Inter')
const { height, lineCount } = layout(prepared, 300, 24)  // 宽度 300px,行高 24px

prepare 做一次性重活:规范化空白、按语义分割文本、应用换行规则、用 Canvas 测量每段宽度、缓存结果,返回一个不透明句柄。layout 就纯计算,输入最大宽度和行高,立刻吐高度和行数。注意 font 参数必须和 CSS 里 font 声明完全一致(大小、字重、家族),行高也要对齐,不然精度会飘。

如果你的文本是 textarea 那种,要保留空格、tab、硬换行可见,就加选项:

typescript 复制代码
const prepared = prepare(textareaValue, '16px Inter', { whiteSpace: 'pre-wrap' })

性能数据(官方基准):prepare 对 500 条混合文本批次大概 19ms,layout 同一批次只要 0.09ms。实际项目里,你在组件 mount 时 prepare 一次,resize 或宽度变化时只跑 layout,帧率直接起飞。

我自己测过类似场景,以前用 DOM 测量 + requestAnimationFrame 节流,1000 条动态高度消息列表滚动还偶尔掉帧。现在换 Pretext,虚拟化逻辑简化成线性遍历高度缓存,120fps 稳得一批。这不是小优化,是架构层面的解放。

高级 API 更狠,面向 Canvas、SVG、WebGL、甚至未来服务端渲染:

typescript 复制代码
import { prepareWithSegments, layoutWithLines, walkLineRanges, layoutNextLine } from '@chenglou/pretext'

const prepared = prepareWithSegments(text, '18px "Helvetica Neue"')
const { lines } = layoutWithLines(prepared, 320, 26)  // 固定宽度返回所有行信息
for (const line of lines) {
  ctx.fillText(line.text, 0, y)
  y += 26
}

prepareWithSegments 返回带段信息的结构。layoutWithLines 直接给你每行完整文本和宽度。walkLineRanges 更底层,只给宽度和光标位置,不拼接字符串,适合二分搜索最优容器宽度(比如聊天气泡 shrinkwrap)。layoutNextLine 是迭代器,能处理变宽场景,比如文字绕图、杂志多栏、浮动元素:

typescript 复制代码
let cursor = { segmentIndex: 0, graphemeIndex: 0 }
let y = 0
while (true) {
  const width = (y < imageBottom) ? columnWidth - imageWidth : columnWidth
  const line = layoutNextLine(prepared, cursor, width)
  if (!line) break
  // 渲染 line.text
  cursor = line.end
  y += lineHeight
}

这些 API 把文本从"黑盒"变成"可编程对象"。以前 CSS 只能给你最终渲染结果,现在你能提前知道每一行的精确边界、宽度、断点,想怎么玩就怎么玩。

它如何做到精确又快

Pretext 的源码干净到几乎没有废代码(整个库打包后几 KB)。核心思路是用浏览器自己的 Canvas 作为真理源头,预先把文本拆成最小可测量单元,缓存宽度,然后用纯 JS 实现换行逻辑。

prepare 阶段先用 Intl.Segmenter 把文本按 grapheme 切分(不是简单 char,因为 emoji 和组合字符要当一个单元处理)。同时处理 Unicode 空白规则、bidi 方向。接着对每段用 Canvas.measureText 测宽度,缓存下来。作者特别提到用了 AI(Claude Code)迭代几周,对着浏览器真实渲染结果反复比对,确保每一种容器宽度下的换行行为 100% 一致。这就是为什么它能跨浏览器精确:不是自己造轮子,而是"模仿"浏览器引擎最准确的那部分,再把测量结果前置缓存。

布局阶段完全抛弃 DOM。line breaking 算法基于缓存的段宽度,做贪心 + 必要回溯,处理连字符、标点禁则、CJK 不换行等所有边缘 case。walkLineRanges 和 layoutNextLine 内部维护一个 cursor(segmentIndex + graphemeIndex),每次推进时只算当前行能塞多少段,算完立即返回,避免任何字符串拼接开销。

我特别欣赏它对 whiteSpace 的处理。normal 模式下自动合并空白、忽略换行;pre-wrap 模式保留原始格式,却依然用同一套测量引擎。bidi 混合文字(英文 + 阿拉伯文)也无缝,因为它在分割时就考虑了方向性。

缓存机制是性能王牌。clearCache() 可以手动清理,适合频繁切换字体的大应用。所有测量结果共享,多次 prepare 相同文本和字体直接复用。官方 benchmark 里 layout 0.09ms 不是吹的,因为它就是数组索引 + 加减法。

对比以前手写 Canvas 文本布局的痛苦(自己测宽度、自己算换行、自己处理 ellipsis、自己对齐浏览器 quirks),Pretext 直接把这些脏活全包了。你只需要专注业务逻辑。

不止性能

  1. 虚拟化列表。以前 variable height 虚拟列表要么估高、要么 DOM 测量后强制 rerender,滚动时容易闪。现在 Pretext 提前算出所有高度,occlusion culling 变成线性扫描,Masonry 那种不规则网格也能 120fps。演示里几十万文本框滚动丝滑,就是证明。

  2. 布局偏移(CLS)彻底消失。新文本加载时,不再需要等 DOM 渲染再调整锚点。高度已知,scroll position 直接算。

  3. Canvas / SVG / WebGL 渲染一致性。以前 DOM 文字和 Canvas 文字永远对不齐,现在同一份 prepared 数据,两边渲染结果像素级一致。游戏 UI、数据可视化、设计工具直接起飞。

  4. 响应式高级排版。聊天气泡自动 shrinkwrap、最优宽度二分搜索、杂志式多栏动态流式、文字绕图、variable font ASCII art......以前 CSS 做这些要一堆 hack,现在一行 layout 调用搞定。演示里的动态布局 demo,像印出来的杂志,却能实时拖拽窗口自适应。

  5. 编辑器与 AI 内容生成。实时流式输出时,不再卡在测量循环。drop cap、justification(Knuth-Plass 算法已有人基于 Pretext 实现)、自动平衡文本、widows/orphans 处理,全都可编程。社区已经有人做爆炸文字、倾斜阅读、3D 排版、手写动画流式渲染,创意直接爆炸。

  6. 开发体验与 AI 协同。作者设计时就考虑了 AI 友好:API 极简,测量结果可预测。以后 AI 写 UI 组件时,不用再猜文本高度,直接调用 Pretext 验证不溢出。

不是取代 CSS

很多人问:这不就是把 CSS 活抢了吗?比如 CSS Shapes,不是。CSS 依然负责最终绘制(如果你用 DOM 渲染),Pretext 只管测量和布局决策。你依然可以用 Tailwind、Flex、Grid 搭框架,但关键的文本高度现在可控了。未来服务端渲染支持后,甚至能把整页排版算好再吐 HTML,hydration 零不匹配。

CSS 的文本模块从来就不是为"精确可编程"设计的。它是声明式、黑盒、浏览器实现的。Pretext 把控制权还给 JS,让你能做 CSS 做不到的:变宽迭代、精确 shrinkwrap、跨平台一致、非矩形流式。两者结合,才是未来。

当然,它不是万能药。目前不直接支持 selection 样式(但演示里已能 overlay 透明 canvas 画选区),复杂排版规则如 full justification 的高级微调还在社区扩展中。但作者开放态度明显,TODO 里写着 server-side、更多语言支持,欢迎 PR。

结论

Pretext 不是一个库,而是一次前端文本能力的重启。它证明了:把浏览器字体引擎的真相前置缓存 + 纯 JS 布局,就能干掉 DOM reflow 这个老大难。作者用 AI 迭代对齐浏览器行为,这条路以后会成为标配。

对普通开发者,它让你写更丝滑的列表、更稳的编辑器;对设计师,它打开了杂志级响应式排版;对 AI 时代,它把文本从"渲染后才知道"变成"生成前就知道",让智能界面真正智能起来。

我强烈建议用 AI 玩一玩 demos。文本不再是瓶颈,而是创意的起点。

参考资料

相关推荐
用户09340777351414 小时前
HarmonyOS WPS Open SDK 实践:用能力分层重构 OpenFileRequest
typescript·harmonyos
gis开发之家1 天前
vue3直击架构痛点 依赖倒置与接口隔离——打造坚如磐石的插件与高阶组件(HOC)模式
javascript·架构·typescript·前端框架·vue3
冬枳1 天前
基于OpenSeagragon封装的前端病理切片展示组件
前端·typescript
退休倒计时2 天前
【每日一题】LeetCode 287. 寻找重复数 TypeScript
算法·leetcode·typescript
@@@@@@@@543 天前
JavaScript 的缺点与 TypeScript 的弥补
开发语言·javascript·typescript
用户0934077735143 天前
HarmonyOS WPS Open SDK 实践:最小打开闭环与 enableEdit 分支处理
typescript·harmonyos
柯克七七3 天前
我把 bug 修得太干净了,测试组以为这个模块本来就没问题
前端·javascript·typescript
退休倒计时3 天前
【每日一题】LeetCode 39. 组合总和 TypeScript
算法·leetcode·typescript
yyt3630458413 天前
KlineChartQuant Tooltip 高频交互 200FPS 性能优化完整复盘
前端·经验分享·性能优化·typescript·vue·交互·chrome devtools
用户0934077735144 天前
HarmonyOS WPS Open SDK 实践:办公审批附件「打开—编辑—回传」代码实现
typescript·harmonyos