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一套给“只需要高度”的场景另一套给“手动控制每一行”的高级玩法。先看简单那套绝大多数业务场景够用import { prepare, layout } from chenglou/pretext const prepared prepare(AGI 春天到了. بدأت الرحلة , 16px Inter) const { height, lineCount } layout(prepared, 300, 24) // 宽度 300px行高 24pxprepare 做一次性重活规范化空白、按语义分割文本、应用换行规则、用 Canvas 测量每段宽度、缓存结果返回一个不透明句柄。layout 就纯计算输入最大宽度和行高立刻吐高度和行数。注意 font 参数必须和 CSS 里 font 声明完全一致大小、字重、家族行高也要对齐不然精度会飘。如果你的文本是 textarea 那种要保留空格、tab、硬换行可见就加选项const prepared prepare(textareaValue, 16px Inter, { whiteSpace: pre-wrap })性能数据官方基准prepare 对 500 条混合文本批次大概 19mslayout 同一批次只要 0.09ms。实际项目里你在组件 mount 时 prepare 一次resize 或宽度变化时只跑 layout帧率直接起飞。以前用 DOM 测量 requestAnimationFrame 节流1000 条动态高度消息列表滚动还偶尔掉帧。现在换 Pretext虚拟化逻辑简化成线性遍历高度缓存120fps 稳得一批。高级 API 更狠面向 Canvas、SVG、WebGL、甚至未来服务端渲染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 是迭代器能处理变宽场景比如文字绕图、杂志多栏、浮动元素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