在 Web 的大部分历史里,"测量一段文本的尺寸"只有一种做法:把它塞进 DOM,调用 getBoundingClientRect() 让浏览器算。能用,但每次调用都会触发一次 layout reflow —— 而 reflow 是客户端渲染中单次开销最大的操作。Pretext 由 Cheng Lou(React 核心团队前成员、react-motion 作者,曾参与 ReScript、Midjourney)设计,给出了完全不同的答案:把文本布局当作纯算术运算来做,从头到尾不碰 DOM。
结果就是一个能跑在每一帧动画里的排版引擎。
Pretext 把文本布局拆成两个阶段:
(文本, 字体) 组合只做一次。通过离屏 Canvas 调用浏览器真实的 measureText() 字体引擎,得到一份小巧的数字"指纹"——字符宽度、可断行位置、segment 边界。性能数据非常直观。500 段文本的场景下:
| 方案 | 耗时 | DOM 操作 |
|---|---|---|
每段调用 getBoundingClientRect() |
~19ms | 500 次 reflow |
每段调用 Pretext layout() |
~0.09ms | 0 |
差不多是 200×–500× 的提速(实际值取决于硬件和场景)。提速不是因为代码更聪明,而是把"昂贵的那类操作"整个从热路径里抹掉了。
它不是一个学院派的玩具,针对的是生产环境里反复出现的痛点。
聊天客户端、日志查看器、信息流 —— 凡是用虚拟滚动的场景,最难处理的都是"行高不固定"。要算出第 N 行的位置,就得知道前 N-1 行有多高,也就是要测量文本。每测一次就 reflow 一次,每次 reflow 都在啃帧预算。
用 Pretext 的话,先用 prepare() 测量一次,之后不管用户怎么 resize、怎么滚动,每一行的 layout() 都只是一次微秒级的算术。行高精确,滚动条不再跳,也没有任何 DOM 往返开销。
瀑布流、被拖拽元素挤开的环绕文本、带障碍物的多栏杂志排版 —— 这些都需要在"提交布局之前"就知道每行的几何信息。CSS 没法回答"这段话在宽度 X 下会有多高",只能渲染出来再测。Pretext 可以在微秒级别内回答任意数量的"假设宽度"。
walkLineRanges() 和 layoutNextLine() 让迭代式布局和变宽布局变得切实可行,包括用二分查找去寻找最优容器宽度。
设计师关心的是"标签会不会被截断",而 DOM 测量只能事后告诉你结果,而且只在你测试机器的字体栈上准确。Pretext 可以用浏览器实际会用的字体度量,在单元测试里、构建脚本里、设计工具里直接断言布局约束。
CLS(Cumulative Layout Shift)是 Google Core Web Vitals 中的关键指标,而异步加载的文本"加载完把下面内容顶一下"是 CLS 的主要来源之一。如果你提前就知道最终高度(因为你在不渲染的情况下已经测过了),就能把空间精确地预留出来。不需要骨架屏靠猜,不会有错位的占位符,更不会有跳动的版面。
API 表面小到可以背下来:
import { prepare, layout, layoutWithLines, walkLineRanges, layoutNextLine } from '@chenglou/pretext';
// 1. 一次性测量
const prepared = prepare("你好,世界 — 这就是 Pretext。", "18px Georgia");
// 2. 热路径布局:只返回高度和行数
const { height, lineCount } = layout(prepared, 400, 1.5);
// 3. 渲染时需要逐行数据?
const { lines } = layoutWithLines(prepared, 400, 1.5);
// 4. 迭代每一行而不构建字符串
walkLineRanges(prepared, 400, (start, end, lineIndex) => { /* ... */ });
// 5. 变宽(例如绕开障碍物)
const line = layoutNextLine(prepared, lastEnd, currentMaxWidth);
只有 prepare() 是相对"贵"的调用 —— 即使如此,笔记本上 500 段不同文本也只要 ~19ms。layout() 快到可以放进 requestAnimationFrame 里,对每一可见行都跑一次。
Pretext 是为全球化排版的混乱现实设计的:
white-space: pre-wrap 模式,适合 textarea 类的、保留空白的内容。它并不想做一个完整的字体渲染栈 —— 它是一个布局引擎。渲染目标随你选:DOM、<canvas>、SVG、WebGL,甚至 Node.js 服务端。
Pretext 有一个不那么显眼但非常重要的属性:布局结果是确定的,并且可以从 JavaScript 直接检查。这意味着 LLM(或者任何代码生成器)可以生成一段 UI,问 Pretext "这段文本会不会溢出?",再迭代一次 —— 不需要 headless browser,不需要靠截图反馈。对自动化 UI 生成、布局感知的代码审查、设计系统的 lint 工具来说,这是台阶式的变化。
npm install @chenglou/pretext
不依赖 DOM,没有 peer dependency,没有框架绑定。在 React、Vue、Svelte、Solid 和原生 JS 里行为完全一致。
最能说明价值的是别人用它做了什么。pretext.cool 社区展示 目前收录了 18+ 个交互式 demo —— 用字符搭出来的俄罗斯方块和打砖块、被拖拽元素实时挤开的环绕文本、带正确透视关系的星球大战开场字幕、绕着会动的钟表流动的杂志排版、用摄像头追踪人脸来响应的字体动画。
这些都不是传统 DOM 测量做得出来的,也不是 CSS 单独能做的。它们之所以可能,是因为文本布局终于快到可以作为"每一帧都能用"的原语。
如果你想动手,快速入门指南 用不到五分钟就能带你跑出第一个布局,Pretext Playground 让你在浏览器里直接试 API。
本文受社区对 Pretext 的介绍文章启发后重新组织撰写。库由 chenglou 出品 —— 社区展示在 pretext.cool。