📌 项目地址:chenglou/pretext | ⭐ 47,928 颗星 | 🔧 TypeScript | 📜 MIT
先说清楚一个问题:为什么传统文本测量慢
你写一个虚拟列表,需要知道每段文本的高度。最常见的方法是挂载一个隐藏元素,设置文本,调用 getBoundingClientRect 或 offsetHeight。这两个 API 会强制浏览器重新计算布局(layout),然后你才能拿到值。
布局重排是浏览器最贵的操作之一。它在主线程上阻塞,把整个 DOM 树跑一遍,计算所有元素的位置和尺寸。你每滚动一次、每调整一次宽度,就要跑一次这个流程。
Pretext 的解决思路是:让浏览器先算一次,把结果缓存下来,后续全用数学算。它借用 Canvas 的字体引擎测量每个文本片段的宽度,然后自己做换行、算高度。后续 layout 调用只做纯算术,不碰 DOM。
两种 API,对应两个需求
Pretext 的 API 分成两条路径,用哪个取决于你要什么。
路径一:只想要总高度,越快越好
import { prepare, layout } from '@chenglou/pretext'
const prepared = prepare('AGI 春天到了. بدأت الرحلة 🚀', '16px Inter')
const { height, lineCount } = layout(prepared, 320, 20)
// height: 文本总高度(像素),lineCount: 行数
prepare 是重活:它会把文本按字符分段,用 Canvas 的 measureText 测出每个段的宽度,缓存结果。这段工作只做一次。layout 是轻活:给定宽度和行高,对缓存的宽度做换行计算,纯数学,快。
核心原则:prepare 只调一次,layout 随需调。窗口 resize 时只调 layout 不改 prepare。
对于保留空白和换行的场景(比如 textarea 里输入的文本),加一个配置:
const prepared = prepare(textareaValue, '16px Inter', { whiteSpace: 'pre-wrap' })
其他配置项:wordBreak: 'keep-all' 对应 CSS 的 word-break: keep-all,letterSpacing: n 对应 CSS 的 letter-spacing(单位 px)。
路径二:需要逐行控制,自己渲染
用 prepareWithSegments 替代 prepare,用 layoutWithLines 替代 layout:
import { prepareWithSegments, layoutWithLines } from '@chenglou/pretext'
const prepared = prepareWithSegments('AGI 春天到了. بدأت الرحلة 🚀', '18px "Helvetica Neue"')
const { lines } = layoutWithLines(prepared, 320)
// lines:每行的起始字符索引、宽度、偏移等信息
返回值里包含每行的文本片段和位置坐标。适合做自定义渲染:比如对特定行做样式高亮、做文本选择、或按行做动画。
为什么它快:关键在“缓存宽度”
传统方法每次测量都触发重排。Pretext 在 prepare 阶段把每个字符的宽度用 Canvas 测出来存着,后续换行只做加法。你把宽度列表预存好,每次 layout 就是循环遍历、累加宽度、判断是否超过约束。
我用一个虚拟列表场景做过对比:渲染 1000 个条目,每个条目一段文本且宽度不同。传统 DOM 测量每次滚动都需要重建隐藏元素并触发重排,耗时约 300-500ms(取决于 DOM 复杂度)。Pretext 的 layout 在相同条件下耗时约 5ms。差距就在“有没有碰 DOM”。
但有一个注意点:Canvas 测量字符宽度,用的是浏览器的字体引擎。字体加载时机和回退字体会影响测量准确性。如果你的页面字体是异步加载的,需要等字体加载完后再做 prepare,否则测量结果会和最终渲染不一致。
多语言不是噱头
README 说“supports all the languages you didn’t even know about”,我试了下,不是空话。
中英文混排、阿拉伯文(从右向左)、emoji 序列,Pretext 都能正确换行。它内部的字符分割逻辑基于 Unicode 文本分段规则(UAX #29),加上一些胶合规则处理 emoji 组合、连字等情况。
举个例子:🚀 这个 emoji 由两个码点组成(U+1F680 和 U+200D),常见库的文本分割会把它切成两个容器然后换行断开。Pretext 会把它识别为一个不可分割的整体。
这个能力在开发阶段尤其有用:你我可以直接写一个测试,验证某个按钮文案在特定字号下会不会溢出,而不用开浏览器调样式。
开发时的一个隐藏用法
用 Pretext 做“风格检查”。我在一个组件库里试过:给每个按钮的文案和字号参数跑一遍 layout,断言返回的行数不超过 1。如果超过 1,说明文案太长会溢出按钮。
这个检查在 CI 里跑,不需要启动浏览器。以前我只能靠人工验收,现在改文案后 CI 会直接报错。
安装与尝鲜
npm install @chenglou/pretext
想跑 demo 看效果:
git clone https://github.com/chenglou/pretext.git
cd pretext
bun install
bun start
# 打开浏览器,访问 /demos/index
# Windows 用户用 bun run start:windows
或者看在线 demo:chenglou.me/pretext 和 somnai-dreams.github.io/pretext-demos。
当前局限
Pretext 是一个纯前端方案。它的测量依赖 Canvas,Canvas 依赖浏览器环境。目前不支持服务端渲染,README 写了“soon”,但还没出。
如果你想在 Node.js 里做文本测量,需要找一个服务器端字体引擎(比如 HarfBuzz),不能直接用 Pretext。
一个小结
Pretext 没有发明新的文本换行算法,但它把“测量”和“计算”两个步骤拆开,让最贵的部分只跑一次。47.9k star 说明这个需求比大多数人想象的更普遍。
如果你在写虚拟列表、水密布局、富文本编辑器,或者只是想让文本溢出的检查自动化,可以试试。