资源推荐
pretext-dom-free-text-layout
pretext-dom-free-text-layout

虚拟列表里计算文本高度,不碰 DOM 也能做到,这个库 34.7k Star

做虚拟列表的时候,如果每行文本高度不固定,就会遇到一个头疼的问题:不渲染出来,怎么知道高度是多少?

传统做法是把文字塞进页面,等浏览器渲染完,再去读取高度,然后再做后续计算。看起来没问题,但这背后有个坑——每次读取 DOM 的布局属性,浏览器都必须先做一次完整的重排。几百行数据挨个量下来,性能直接拉跨。

pretext (opens in a new tab) 的核心骚操作是:完全跳过 DOM,在内存里用 Canvas 预计算文本高度。速度比传统方案快几十倍,而且准得惊人——它能完美模拟各大浏览器的原生渲染效果,支持多语言混排、表情符号,甚至双向文本(阿拉伯语、希伯来语那种从右往左的)。

这意味着你可以在 Canvas、SVG,甚至服务端,算出和浏览器里一模一样的文字排版结果。

为什么 DOM 测量这么贵

浏览器不会实时更新布局,它会尽量把计算推迟到必要时候才做。但如果你在 JavaScript 里读取 offsetHeightgetBoundingClientRect() 这类属性,浏览器没办法,必须立刻把当前所有待计算的布局全部跑完,才能给你返回一个准确的值。

这叫 Forced Synchronous Layout(强制同步布局),是 Web 性能里很经典的坑。在循环里反复触发,每次都是一次完整 Layout,几百个文本量下来就是几百次重排。

Canvas 的 measureText() API 是另一条路——它可以获取字体相关的度量数据,整个过程不走 DOM Layout,自然也不会触发重排。pretext 就建在这个基础上,自己实现了完整的多行换行算法。

两步分离:分析一次,计算随便跑

pretext 把整个过程拆成两步,这个设计很关键:

第一步 prepare(text, font)

用 Canvas 测量字符宽度,处理 Unicode 分词、表情符号、双向文本等复杂情况,把分析结果缓存下来。这一步稍慢,500 个文本大约 19ms,但只需要跑一次。

第二步 layout(prepared, maxWidth, lineHeight)

拿缓存的分析结果做纯数学运算,不再碰 Canvas 或 DOM,直接算出高度和行数。这一步极快,500 个文本约 0.09ms,可以随便跑。

import { prepare, layout } from '@chenglou/pretext'
 
// 文本分析,结果可以缓存
const prepared = prepare('需要测量的文本内容...', '16px Inter')
 
// 纯数学计算,在 resize 事件里高频调用也没问题
const { height, lineCount } = layout(prepared, maxWidth, lineHeight)

为什么要拆开?因为文本内容变了才需要重新 prepare,但容器宽度变了(比如用户拖动侧边栏)只需要重跑 layout,不用重新分析文本。这样热路径上的开销降到最低。

在 Canvas 里做文字排版

如果你要在 Canvas 或 SVG 里渲染文本,pretext 还提供了逐行布局的 API——直接告诉你每一行从哪个字符到哪个字符:

import { prepareWithSegments, layoutWithLines } from '@chenglou/pretext'
 
const prepared = prepareWithSegments(text, font)
const { lines } = layoutWithLines(prepared, width, lineHeight)
 
lines.forEach((line, i) => {
  const lineText = text.slice(line.start, line.end)
  ctx.fillText(lineText, x, y + i * lineHeight)
})

更厉害的是 layoutNextLine(),支持每行宽度不一样的场景——比如文本要环绕一个图片,左边几行短,图片下面又变宽,这种在 DOM 里靠 float 实现的效果,在 Canvas 里完全没法用,就得靠这个:

const state = initLayoutState(prepared)
let y = 0
 
while (!state.done) {
  const availableWidth = getWidthAtY(y)  // 根据 y 位置动态算可用宽度
  const line = layoutNextLine(state, availableWidth, lineHeight)
  drawLine(line, y)
  y += lineHeight
}

服务端渲染同理——Node.js 环境里没有 DOM,但可以用 node-canvas 跑 Canvas API,pretext 同样可以工作,输出和浏览器端一致的文字布局结果。

几个要注意的地方

pretext 对应标准 CSS 文本行为:white-space: normalword-break: normaloverflow-wrap: break-word,也支持 pre-wrap

不支持富文本(一段文字里混多种字体或字号),每次调用只能传一个字体。

macOS 上用 system-ui 字体可能有轻微精度问题,建议用具名字体(InterRoboto 等)。

安装:

npm install @chenglou/pretext

写在最后

pretext 瞄准的问题很明确:在不触发 DOM 重排的前提下,算出文本高度或行分布。虚拟列表动态行高、Canvas/SVG 文字排版、服务端预计算布局,这几个场景下它是目前比较完整的方案。

prepare + layout 两步分离的设计之外,它对浏览器字体渲染细节的处理也值得参考——多语言、emoji、双向文本这些边界情况,自己从头处理起来相当繁琐,pretext 帮你踩完了这些坑。

GitHub:https://github.com/chenglou/pretext (opens in a new tab)