📰 正文

前 React 核心团队成员、ReasonML 作者、现 Midjourney 工程师 Cheng Lou 开源了 Pretext,一个纯 TypeScript 写的文本测量和排版库。

它能在完全不碰 DOM 的情况下精确计算文本高度、行数、换行位置,让网页排版绕过浏览器里最贵的操作:布局回流(layout reflow)。

核心就一句话:能让你不用真的把文字放到网页上,就能提前知道文字会占多大空间。

先打个比方。

你装修房子,买了一个书架,想知道放在客厅那面墙能不能放得下。正常人会拿尺子量一下墙的宽度和书架的宽度,对比一下就知道了。

但浏览器不是这么干的。浏览器的做法是:把书架搬过去,塞进客厅,然后看看放不放得下。放不下?搬走,换个位置再塞一次。每次你想知道"放不放得下",它就搬一次家具。

这就是为什么网页有时候会"闪"一下,聊天列表滑着滑着会"跳"一下。

浏览器在反复搬家具。

Pretext 做的事情就是给浏览器一把尺子。

量一下就知道了,不用搬。

它用纯数学计算文字的高度和行数,不需要真的把文字放到网页上去排版。500 段文字的计算只要 0.09 毫秒,比浏览器"搬家具"快几百倍。

GitHub 一天 6000+ star,推文 780 万浏览、3.4 万点赞、3.6 万收藏。

Cheng Lou 自己的原话是"I have crawled through depths of hell to bring you this"(我从地狱深处爬出来把这东西带给你们),语气夸张,但看完 Demo 你会觉得他没吹。

Pretext 实时文字排版的效果

Web 排版 30 年的老毛病

做过前端的都知道这个痛:想知道一段文字占多高、哪里换行,你得把文字塞进 DOM,让浏览器排一遍版,再用 getBoundingClientRect 或 offsetHeight 读数值出来。

这叫布局回流,是浏览器最贵的操作之一。改文字、调宽度、加元素,浏览器可能重新算整个页面布局。

很多高级排版效果需要提前知道文字尺寸:

瀑布流要知道每个卡片多高,聊天气泡要知道最紧凑的宽度,虚拟长列表要知道每一项占多少空间,文字绕图要知道每行该放几个字。

传统做法要么粗略估算忍受跳动,要么触发大量回流拖垮性能。这个困境从 CSS 1.0 到今天,30 年了。

Pretext 的做法

思路很直接:把文本测量从 DOM 里彻底抽出来。

prepare() 用 Canvas 的 measureText 做一次性的文字测量(这一步不触发回流),把文本分段、应用换行规则、缓存每段宽度。之后调 layout(),所有计算都是纯数学运算,不再碰 DOM。

import { prepare, layout } from '@chenglou/pretext'

const prepared = prepare('AGI 春天到了. بدأت الرحلة 🚀', '16px Inter')
const { height, lineCount } = layout(prepared, 320, 20)
// 320px 宽、20px 行高下的精确高度和行数,不碰 DOM

两行代码。性能差距很大:500 段文本的批次测试里,prepare() 总共 19ms(一次性开销),layout() 只要 0.09ms。算完 500 段文字的高度,连 0.1 毫秒都不到。

它还处理了各种语言的边缘情况:中文、日文、阿拉伯文(从右到左)、emoji、混合双向文本,全部支持。README 示例里同时出现中文、阿拉伯文和 emoji,实测都能正确处理。

不只是量高度

高度测量只是入门。Pretext 的 API 分两层,第二层让你手动控制每一行的排版,能做的事情比想象中多很多。

文字绕图: 传统 CSS 的 float 能让文字绕图,但控制力极有限。Pretext 的 layoutNextLine() 可以逐行排版,每行给不同宽度。图片旁边的行窄一点,图片下面恢复全宽,文字像杂志一样自然地绕着图片流动。

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
}

杂志和报纸排版里最基本的文字绕图,在浏览器里终于能干净地实现了。

消息气泡收缩包裹: 聊天界面里消息气泡宽度怎么定?太宽浪费空间,太窄多余换行。walkLineRanges() 能找到"保持行数不变的最窄宽度"。这个多行收缩包裹能力,Web 原生一直缺。

虚拟列表不用瞎猜高度: 做过长列表虚拟化的都知道,最头疼的就是"每一项多高"。以前要么固定高度(丑),要么先渲染再测量(慢),要么给估算值忍受跳动。现在渲染之前就精确知道高度了。

开发时校验文字溢出: 按钮上的文字会不会换行?标签会不会被截断?以前靠浏览器跑一遍才知道。Pretext 让你在构建阶段就能验证,甚至可以丢给 AI 批量检查,不需要浏览器环境。

普通用户能感受到什么

你可能觉得这是前端开发者才关心的事。但你每天都在被这个问题影响:

聊天列表不"跳"了。 微信、飞书这类应用,滑动聊天记录时偶尔会突然跳一下,因为消息高度算错了。有了 Pretext,不用渲染就能精确算出每条消息的高度,列表就稳了。

网页不"闪"了。 你打开一篇文章,内容加载出来后页面往下一跳,你正在看的东西跑到了别的位置。因为浏览器一开始不知道文字有多高。Pretext 能提前算好,预留空间,页面就不跳了。

消息气泡不浪费空间了。 你发一段长消息,气泡宽度按最长那行来,最后一行很短的时候后面全是空白。CSS 做不到"找到保持同样行数的最窄宽度"。Pretext 能算出来,气泡每个像素都不浪费。

AI 生成界面时知道文字会不会溢出。 AI 生成了一个按钮写着"立即获取限时优惠",在手机上放不放得下?以前只能渲染出来才知道。有了 Pretext,生成阶段就能算出来,超了就换措辞。

性能

在当前基准测试中:

image

layout() 比 prepare() 快 200 倍,因为它完全不碰浏览器。你可以在一帧(16ms)内对几千段文字重新计算布局。

语言支持

支持所有语言,包括中文、日文、韩文、阿拉伯文(RTL)、混合双向文本、emoji。Demo 里用的示例文字就是中英阿混合加 emoji 的。

支持 pre-wrap 模式(保留空格、tab、换行符),适合 textarea 场景。

安装

npm install @chenglou/pretext

MIT 协议,免费开源。

这件事的意义

Pretext 做的事情看起来很小(测量文字高度),但它打开了一扇门:让前端开发者能在 DOM 之外做布局计算。

以前你想做任何涉及"文字有多高"的计算,都绕不开 DOM 和 reflow。Pretext 把这个依赖切断了。文本测量变成了纯函数,可以在 Web Worker 里跑,可以在渲染前跑,可以在 AI 生成 UI 的时候跑。

对 AI 生成界面这个方向来说,这可能是一个关键的基础设施。AI 生成一个按钮上的文字,目前没有办法在不渲染的情况下知道文字会不会溢出。有了 Pretext,这个验证可以在生成阶段就完成。

Demo 值得看一遍

Pretext 的在线 Demo 有七个场景,每个都像"不该在浏览器里存在的东西":

手风琴折叠:展开收起高度提前算好,动画丝滑不抖

气泡消息:紧凑的多行气泡,同样的文字占更少面积

动态排版:障碍物感知的标题路由,文字连续流动

编辑引擎:实时文字重排、拉引、多栏排版,全程零 DOM 测量

富文本:内联代码、链接、标签混排,标签整体不被拆行

瀑布流:用 Pretext 预测高度代替 DOM 读取

ASCII 字符画:用比例字体做粒子驱动的 ASCII 艺术

渲染目标不限于 DOM,Canvas、SVG、WebGL 都行,服务端渲染在路线图上。

👉 Cheng Lou 原推 | GitHub | 在线 Demo


来源:Pretext: 纯 TypeScript 文本测量引擎 解锁 30 年来 Web 做不到的排版问题