我以前一直把“文本测量”当成前端里的脏活:临时挂一个隐藏节点,读一次高度,删掉节点,继续写业务。 直到有天聊天列表在真机上连续掉帧,我才意识到:我们不是在测文字,我们在反复打断浏览器的布局流水线。
pretext 的爆火,不是因为它又造了一个“测高函数”。 它真正击中的,是前端一个长期被忽略的高频痛点:把文本布局这件事,从 DOM 读写里剥离出来,变成可缓存、可复用、可预测的纯计算。
金句:性能问题的本质,常常不是“算太慢”,而是“算错了地方”。
01 为什么 pretext 会突然爆:它动的是浏览器最贵的一段路径
在传统方案里,我们为了拿到多行文本高度,通常会走这条链路:
- 写入文本到 DOM
- 触发布局计算
- 读取 offsetHeight 或 getBoundingClientRect
- 宽度变化后再来一遍
这条路的问题不是“不能用”,而是它在高频场景里代价陡增。比如虚拟列表、聊天会话流、AI 实时生成文案预览,只要宽度、字体、内容有变化,就会不断触发 reflow。你以为只是读了一个高度,实际上让浏览器把前后文的布局账单都结了一次。
pretext 的思路是反着来:尽量在计算层解决问题,不把浏览器渲染引擎拉进每一次循环。
Before:每次测量都依赖 DOM 状态。 After:先做一次准备,后续只做纯计算。
金句:把高频路径从“读布局”改成“算布局”,才是前端性能优化的分水岭。
02 它到底做了什么:prepare 一次,layout 多次
pretext 的核心设计是双阶段:
- prepare:一次性做文本分段、空白规则处理、测量并缓存
- layout:在给定宽度和行高下,快速计算高度与行数
官方 README 给出的基准(500 条文本批次)是:
- prepare 约 19ms
- layout 约 0.09ms
这组数字的意义不在“绝对快到离谱”,而在“冷热路径拆分成功”:
- 冷路径允许稍重,因为执行次数少
- 热路径必须极轻,因为会反复触发
这和我们在前端做的图片解码缓存、列表虚拟化、请求去重,本质是同一种工程思维:一次准备,多次消费。
金句:真正的优化,不是把每一步都做快,而是让最常走的那一步足够便宜。
03 这不是玩具库:它正好命中四类高价值场景
场景A:虚拟列表动态高度
你最怕的是估高不准导致滚动跳动。pretext 可以在渲染前预测高度,减少“先猜再纠正”的抖动链路。
场景B:聊天与评论流
消息内容、语言、emoji 混排复杂,传统隐藏节点测量会放大性能抖动。pretext 对多语言和 mixed-bidi 处理更稳,适合高并发文本流。
场景C:Canvas 或 SVG 或 WebGL 文本布局
这些场景本来就不依赖 DOM 文本节点。pretext 提供了按行输出和逐行推进能力,天然适配自绘渲染。
场景D:AI 生成 UI 的预校验
在“先生成后渲染”的工作流里,你可以先做离线布局校验,提前发现按钮文案换行、卡片标题溢出等问题,减少回归成本。
金句:当业务进入“文本即数据流”阶段,布局必须从渲染副作用里独立出来。
04 实战接入:用在 React 列表里,别把收益写没了
下面这段是一个可直接改造的思路(关键在缓存 prepared):
import { prepare, layout } from "@chenglou/pretext";
const preparedCache = new Map();
function getPrepared(text, font) {
const key = font + "::" + text;
if (!preparedCache.has(key)) {
preparedCache.set(key, prepare(text, font));
}
return preparedCache.get(key);
}
export function measureMessageHeight(text, width) {
const font = "14px PingFang SC";
const lineHeight = 22;
const prepared = getPrepared(text, font);
const result = layout(prepared, width, lineHeight);
return result.height;
}
落地时有三个动作不能省:
- 字体声明必须和真实渲染一致(字号、字重、字族)
- lineHeight 必须和 CSS 保持一致
- resize 时优先重跑 layout,不要反复重跑 prepare
金句:你以为在优化库,真正优化的是“调用方式”。
05 先把边界看清:哪些坑会让你误判 pretext
pretext 不是完整字体引擎,它有明确边界,理解边界比盲目神化更重要:
- 默认目标接近 white-space normal、word-break normal、overflow-wrap break-word 这组常见网页配置
- 需要保留空格、制表符、换行时,要显式开 whiteSpace pre-wrap
- macOS 上 system-ui 对精度不安全,建议使用命名字体
- 极窄宽度下会在字素边界内断词,这是默认换行策略带来的可预期行为
如果你的业务是高级排版软件级需求,仍要做更重的排版系统;但如果你是互联网产品中的高频文本布局,这个边界已经覆盖绝大多数核心场景。
金句:工程价值从不等于“全能”,而在于“对主战场足够强”。
06 我对这波 pretext 的判断:它是方法论信号,不只是新库红利
我更在意的,不是“又多了一个 npm 包”,而是它提醒前端团队一件事: 我们习惯把布局问题交给浏览器兜底,但在高频业务里,布局本身就是业务性能的一部分。
pretext 给出的答案是:
- 把高频测量从渲染副作用中抽离
- 把一次性成本前置
- 把热路径压到可忽略级别
这套方法不会只停在文本领域。它会继续影响我们处理图片裁切、卡片排布、可视化标注、甚至 AI 驱动 UI 生成的方式。
如果你正被列表抖动、消息流掉帧、布局回流困住,pretext 值得你今天就开一个分支实测。 别先问“它能不能替代一切”,先问“它能不能救你最贵的那段路径”。
07 实现原理拆解:pretext 怎么把“排版”变成“计算”
pretext 的核心不是一个“测高函数”,而是一条四层流水线:
输入文本 -> 文本分析(analysis)-> 宽度测量(measurement)-> 断行决策(line-break)-> 输出行数/高度
1)analysis:先把自然语言变成可计算 token
prepare() 的前半段先做结构化,而不是直接测宽:
- 按
white-space规则归一化(normal/pre-wrap) - 用
Intl.Segmenter切分(覆盖 CJK、泰语、阿拉伯语) - 给每段打上 break kind:
text/space/tab/hard-break/soft-hyphen/zero-width-break/glue - 做浏览器行为导向的合并:URL 连段、数字串、标点链、CJK 禁则、阿拉伯/缅文粘连规则
这一层决定了“断行质量上限”。analysis 对,后面才有可能又快又准。
金句:性能优化的第一步,不是算得更快,而是先把问题描述正确。
2)measurement:把 token 变成宽度数组(并缓存)
prepare() 后半段用 canvas 测量,不碰 DOM 布局树:
- 用
OffscreenCanvas/canvas.measureText测宽 - 以
(font, segment)做缓存,避免重复测量 - 对可断长词预计算 grapheme 宽度(
breakableWidths) - 维护两套行尾宽度:
lineEndFitAdvances(判定能否放入)与lineEndPaintAdvances(最终绘制宽度)
关键细节是 emoji 校正:在 Chromium/Firefox 的某些字体尺寸下,canvas 对 emoji 可能偏宽,pretext 会做一次 canvas 与隐藏 DOM span 的校准,把差值缓存成 emojiCorrection。
金句:不是所有误差都要消灭,但高频误差必须被驯服。
3)line-break:状态机断行(layout() 的核心)
layout() 本质是跑一个轻量状态机,核心状态很少:
- 当前行宽
lineW - 最近合法断点
pendingBreak - 当前游标
(segmentIndex, graphemeIndex)
决策顺序大致是:
- 先尝试整段放入当前行
- 超宽优先回退到最近合法断点
- 无断点且为长词时,按 grapheme 粒度硬断(
overflow-wrap: break-word) - 命中
soft-hyphen时补上可见-的宽度 tab、hard-break、行尾空白走专门分支
此外还有 EngineProfile 做浏览器差异收敛(Safari/Chromium 的 epsilon 与策略差异)。它不是理想化排版器,而是贴近真实浏览器行为的工程实现。
金句:断行不是数学题,而是带浏览器个性的工程博弈。
4)为什么它能快:冷热路径拆分
prepare(text, font):重活前置(分析、测量、缓存)layout(prepared, maxWidth, lineHeight):热路径只做数组遍历与加减比较
所以 resize 时只重复 layout,不重测文字、不触发 DOM reflow。性能收益来自“重活前置 + 热路径极简”。
5)一段伪代码看完整链路
const prepared = prepare(text, font)
// 内部:analysis -> measurement -> cache arrays
const { lineCount, height } = layout(prepared, width, lineHeight)
// 内部:line-break state machine only
// width 变化时:
const next = layout(prepared, nextWidth, lineHeight)
// 不重新测量,不碰 DOM
6)工程启发
pretext 的方法论可以迁移到更多前端场景:先把“渲染副作用问题”改写为“可缓存的数据问题”,再让高频路径退化成纯计算。
收束金句:当布局从副作用变成数据流,性能就从玄学变成工程。
参考链接:
- GitHub: github.com/chenglou/pr…
- npm: www.npmjs.com/package/@ch…
收束金句: 把重复测量交给预计算,把浏览器主线程还给真正的交互。