Pretext:3周28k Star,这个库解决了前端性能的隐形杀手
前端性能优化,我们习惯关注网络请求、图片压缩、代码分割。但有一个性能杀手藏得很深——文本测量触发的 Layout Reflow。
当你调用 getBoundingClientRect()、读取 offsetHeight,或者在循环里频繁查询 DOM 尺寸,浏览器被迫执行强制同步布局。这是一次昂贵的"停顿",主线程阻塞,页面卡顿,用户体验崩塌。
上周,一个叫 Pretext 的库悄然发布,3周内斩获 28,000+ Star。它用纯 JavaScript 解决了这个困扰前端开发多年的难题。
传统文本测量的困境
先看一个常见场景:虚拟列表。
虚拟列表的核心在于"预知高度"。你需要知道每个列表项的高度,才能计算滚动位置、决定渲染哪些项。
问题来了:动态内容的高度无法预知。
一段文本在不同宽度下会换行,高度随之变化。传统做法是:
// 先渲染到 DOM
element.textContent = text;
element.style.width = `${containerWidth}px`;
// 再读取高度 —— 触发 reflow
const height = element.offsetHeight;
这有什么问题?
Layout Reflow 是浏览器最昂贵的操作之一。 它需要重新计算整个页面的布局树,代价与 DOM 复杂度成正比。在虚拟列表场景,你可能需要测量几十上百个元素,每次测量都是一次性能惩罚。
更糟的是,当你需要响应式布局——窗口宽度变化时重新测量——性能问题会被放大。瀑布流、AI 流式输出、富文本编辑器,这些场景都逃不开文本测量。
Pretext 的解法:绕过 DOM,纯算法测量
Pretext 的核心思路很简单:不触碰 DOM,用纯 JavaScript 计算文本高度。
它的原理是利用 Canvas 的 measureText API 获取字符宽度,结合自己的换行算法,直接算出文本在不同宽度下的高度。
import { prepare, layout } from '@chenglou/pretext'
// 1. 预处理:分析文本结构,测量字符宽度
const prepared = prepare('这是一段测试文本', '16px "Microsoft YaHei"')
// 2. 布局计算:纯算术运算,零 DOM 操作
const { height, lineCount } = layout(prepared, 320, 24)
// height: 48, lineCount: 2
关键在于两阶段分离:
prepare():一次性分析文本,缓存字符宽度layout():纯算术运算,根据容器宽度计算高度
这意味着:窗口 resize 时,只需要重新执行 layout(),耗时从毫秒级降到微秒级。
实战演示:零 DOM 读取的响应式布局
下面是一个基于 Lit + Pretext 的实战示例,展示如何在不触碰 DOM 的情况下实现响应式高度计算:
import { LitElement, html } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { prepare, layout } from '@chenglou/pretext';
const textContent = '春天到了,万物复苏。这是一段用于测试纯算法预测高度的文本。随着宽度的改变,它的行数和高度也会随之发生改变,且全程不会触发基于DOM读取的强制重排。';
@customElement('pure-algo-element')
export class PureAlgoElement extends LitElement {
@state() containerWidth: number = 400;
@state() predictedHeight: number = 0;
@state() predictedLines: number = 0;
// 预处理:只要文本和字体不变,只做一次
private preparedText = prepare(textContent, '16px "Microsoft YaHei"');
connectedCallback() {
super.connectedCallback();
this.calculateLayout(this.containerWidth);
}
// 纯内存计算,不触碰真实 DOM
calculateLayout(width: number) {
const { height, lineCount } = layout(this.preparedText, width, 24);
this.predictedHeight = height;
this.predictedLines = lineCount;
}
handleWidthChange = (e: Event) => {
const newWidth = Number((e.target as HTMLInputElement).value);
this.containerWidth = newWidth;
console.time("算法执行耗时");
this.calculateLayout(newWidth);
console.timeEnd("算法执行耗时");
}
render() {
return html`
<div class="container">
<input type="range" min="100" max="500"
.value=${this.containerWidth}
@input=${this.handleWidthChange} />
<div class="result">
高度: ${this.predictedHeight}px | 行数: ${this.predictedLines}行
</div>
<div class="text-box" style="width: ${this.containerWidth}px; height: ${this.predictedHeight}px;">
${textContent}
</div>
</div>
`;
}
}
运行效果:

拖动滑块改变宽度,高度实时计算,整个过程零 DOM 读取。
性能对比:差距肉眼可见
官方 benchmark 数据:
| 操作 | 耗时 |
|---|---|
prepare() (500段文本) | ~19ms |
layout() (500段文本) | ~0.09ms |
我们用实际代码测试,单次 layout() 执行耗时:

注意:这只是单个 DOM 元素的差距。 在真实场景中,当页面有几十上百个需要测量的元素时,性能差距会被放大数十倍。
传统方案的问题在于:
循环 N 个元素 {
渲染到 DOM
读取 offsetHeight ← 触发 reflow
}
每次读取都触发一次 reflow,N 个元素就是 N 次昂贵的布局计算。
Pretext 的方案:
循环 N 个元素 {
调用 layout() ← 纯算术运算
}
N 次计算,但全是内存操作,无任何 reflow。
三大核心场景
场景一:虚拟列表
虚拟列表的核心难题:如何预知动态内容的高度?
传统方案需要"先渲染再测量",导致:
- 初始渲染闪烁
- 滚动位置跳动
- 频繁 reflow
使用 Pretext:
// 列表初始化时,预处理所有文本
const items = data.map(item => ({
...item,
prepared: prepare(item.text, '14px Inter')
}))
// 根据容器宽度,计算每个 item 高度
function getItemHeight(index: number, width: number): number {
const { height } = layout(items[index].prepared, width, 20)
return height
}
完美虚拟化,零闪烁,零 reflow。
场景二:瀑布流布局
瀑布流需要计算每列的高度,决定下一个元素放在哪一列。
传统做法是用 CSS column-count 或 JS + DOM 测量。前者不够灵活,后者性能堪忧。
Pretext 提供了 layoutNextLine() API,支持变宽度逐行布局:
import { prepareWithSegments, layoutNextLine } from '@chenglou/pretext'
const prepared = prepareWithSegments(text, '14px Inter')
let cursor = { segmentIndex: 0, graphemeIndex: 0 }
let y = 0
while (true) {
// 不同行可以有不同的宽度!
const width = y < imageHeight ? columnWidth - imageWidth : columnWidth
const line = layoutNextLine(prepared, cursor, width)
if (!line) break
// 渲染这一行
renderLine(line.text, y)
cursor = line.end
y += lineHeight
}
这让文字环绕图片、复杂网格布局成为可能,而且全是纯 JS 计算,无任何 DOM 依赖。
场景三:AI 流式输出
ChatGPT 类应用的核心体验:流式输出文本。问题在于:
- 文本不断增长,高度不断变化
- 需要实时调整滚动位置
- 频繁的 DOM 测量 = 频繁的 reflow
使用 Pretext 的方案:
// 流式输出时,增量计算高度
function onNewToken(token: string) {
fullText += token
// 只需要重新执行 layout,不需要重新 prepare
const { height } = layout(prepared, containerWidth, lineHeight)
// 更新容器高度,保持滚动锚点
updateContainerHeight(height)
}
高频更新场景下,layout() 的微秒级延迟 vs DOM 测量的毫秒级延迟,差距是数量级的。
API 速览
Pretext 提供两套 API:
场景一:只关心高度
import { prepare, layout } from '@chenglou/pretext'
const prepared = prepare(text, '16px Inter')
const { height, lineCount } = layout(prepared, maxWidth, lineHeight)
场景二:需要逐行控制
import { prepareWithSegments, layoutWithLines, walkLineRanges, layoutNextLine } from '@chenglou/pretext'
const prepared = prepareWithSegments(text, '16px Inter')
// 获取所有行信息
const { lines } = layoutWithLines(prepared, maxWidth, lineHeight)
// 遍历行范围(不构建文本字符串)
walkLineRanges(prepared, maxWidth, line => {
console.log(line.width, line.start, line.end)
})
// 逐行布局(支持变宽度)
const line = layoutNextLine(prepared, cursor, width)
注意事项
Pretext 目前支持的 CSS 属性:
white-space: normal或pre-wrapword-break: normaloverflow-wrap: break-wordline-break: auto
不支持的场景:
- 复杂的 CSS 文本样式(如
text-shadow影响布局) word-break: break-all等特殊断词规则- 使用
system-ui字体(macOS 上精度有问题,建议用命名字体)
总结
Pretext 解决的是一个"不起眼但很痛"的问题——文本测量的性能开销。
它的价值在于:
- 性能提升:将文本测量从 O(DOM复杂度) 降到 O(1)
- 解锁场景:虚拟列表、瀑布流、AI 流式输出,都可以做到真正的"预知高度"
- 开发体验:告别 CSS hack,用 JS 精确控制布局
适用场景:
- 需要预知动态文本高度的虚拟列表
- 瀑布流、网格布局等复杂排版
- AI 流式输出、实时编辑器等高频更新场景
- 需要在服务端计算文本高度(未来支持)
如果你的项目有类似痛点,值得尝试。
GitHub: https://github.com/chenglou/pretext
参考资料
- Pretext 官方文档
- MDN: Layout Thrashing
- Sebastian Markbage 的 text-layout 设计思想
更多推荐
所有评论(0)