⚡Pretext: 无 DOM 布局回流的快速文本测量库

1 阅读3分钟

omQIhBr6VEk3ypQ79KiFq0pbBZ4Ljuu34M6xvp2B.gif

Pretext 是一个纯 JavaScript 文本测量库,通过 Canvas API 缓存字符宽度,支持在不改动 DOM 的情况下快速计算文本高度和行数。适合虚拟列表、动态排版等性能敏感场景。

为什么需要 Pretext?

前端开发中,文本测量是虚拟列表、自适应布局等功能的基石。传统方案需要:

  1. 创建隐藏的 DOM 元素
  2. 插入文本
  3. 读取 offsetHeight/getBoundingClientRect()
  4. 触发浏览器布局计算(Layout)

这种方式在大数据量或频繁更新时性能堪忧。

Pretext 的解决方案: 用 Canvas API 一次性测量所有字符宽度,后续计算纯算术完成,不触发布局回流。

hnLViGKk1B4CVR6S6t7l2uIoVK3VaSBvr8b9cFQk.gif

核心 API

1. prepare + layout — 快速测量

最基础的用法,适合只需要总高度和行数的场景。

import { prepare, layout } from '@chenglou/pretext'

const text = 'AGI 春天到了. Howe est? 🚀'
const font = '16px Inter'
const lineHeight = 24

// 一次性分析文本,返回不透明句柄
const prepared = prepare(text, font)

// 纯算术计算,不触发布局
const result = layout(prepared, 300, lineHeight)

console.log(result.height)     // 总高度
console.log(result.lineCount)  // 总行数
// 输出示例
// { height: 48, lineCount: 2 }

使用场景: 虚拟列表的 item 高度计算、聊天气泡的自适应高度。

2. prepareWithSegments + layoutWithLines — 获取行详情

需要知道每行具体内容的场景。

import { prepareWithSegments, layoutWithLines } from '@chenglou/pretext'

const text = 'Hello World! This is Pretext.'
const prepared = prepareWithSegments(text, '16px Inter')

// 返回每行的详细信息
const { height, lineCount, lines } = layoutWithLines(prepared, 200, 24)

lines.forEach((line, i) => {
  console.log(`Line ${i + 1}: "${line.text}" (${line.width}px)`)
})
// 输出示例
// Line 1: "Hello World!" (81px)
// Line 2: "This is" (42px)
// Line 3: "Pretext." (60px)

使用场景: 文本编辑器行号显示、代码高亮的行对齐。

3. walkLineRanges — 回调遍历

需要逐行处理,每行触发一次回调。

import { prepareWithSegments, walkLineRanges } from '@chenglou/pretext'

const prepared = prepareWithSegments(text, '16px Inter')

// 遍历每一行,执行自定义逻辑
const lineWidths: number[] = []
walkLineRanges(prepared, 300, 24, (line) => {
  lineWidths.push(line.width)
  console.log(`"${line.text}" starts at ${line.start}, ends at ${line.end}`)
})

console.log('All widths:', lineWidths)

使用场景: 查找最长行、收集行统计信息。

4. layoutNextLine — 迭代器模式

逐行获取,可从任意位置开始,适合流式布局。

import { prepareWithSegments, layoutNextLine } from '@chenglou/pretext'

const prepared = prepareWithSegments(text, '16px Inter')
let cursor = { paragraph: 0, secondLine: 0 }

while (true) {
  const line = layoutNextLine(prepared, cursor, 300)
  if (line === null) break

  console.log(`"${line.text}" (${line.width}px)`)
  cursor = line.end  // 关键:使用上一行的结束位置继续
}

使用场景: 流式文本渲染、增量加载文本。

5. whiteSpace: 'pre-wrap' 选项

保留换行和缩进。

const codeText = `function hello() {
  console.log('Hello')
}`

const prepared = prepare(codeText, '14px "Fira Code"', { whiteSpace: 'pre-wrap' })
const { height, lineCount } = layout(prepared, 300, 20)

Vue 3 集成示例

<script setup>
import { ref, reactive, onMounted } from 'vue'
import { prepare, layout, prepareWithSegments, layoutWithLines } from '@chenglou/pretext'

const content = ref('')
const containerWidth = ref(400)
const lineHeight = 24
const font = '16px Inter'

// 文本内容变化时重新计算
function calculateHeight() {
  const prepared = prepare(content.value, font)
  return layout(prepared, containerWidth.value, lineHeight)
}

// 获取行详情
function getLines() {
  const prepared = prepareWithSegments(content.value, font)
  return layoutWithLines(prepared, containerWidth.value, lineHeight)
}

const height = ref(0)
const lineCount = ref(0)
const lines = ref([])

function update() {
  const result = calculateHeight()
  height.value = result.height
  lineCount.value = result.lineCount
  lines.value = getLines().lines
}

onMounted(update)
</script>

<template>
  <div>
    <textarea v-model="content" @input="update" />
    <p>高度: {{ height }}px, 行数: {{ lineCount }}</p>
    <div v-for="(line, i) in lines" :key="i">
      {{ i + 1 }}: {{ line.text }} ({{ line.width }}px)
    </div>
  </div>
</template>

demo预览

somnai-dreams.github.io/pretext-dem…

性能对比

方案1000 次测量耗时是否触发布局
原生 DOM (offsetHeight)~800ms
Pretext (首次 prepare)~50ms一次性
Pretext (后续 layout)~1ms

Pretext 的首次 prepare 稍慢(需测量字符),但后续 layout 调用极快(纯算术)。

注意事项

  1. font 字符串必须匹配:确保 prepare() 的 font 参数与实际 CSS 渲染完全一致,包括字号、字重、字体族。

  2. lineHeight 必须一致layout() 的 lineHeight 参数需与 CSS line-height 声明值相同。

  3. 不支持的 CSS 特性:不支持 letter-spacingword-spacing 扩展、部分 Unicode 字符可能测量不准。

适用场景

  • 虚拟列表/虚拟滚动
  • 聊天应用的消息气泡
  • 动态排版系统
  • 任何需要提前知道文本尺寸的场景

不适用场景

  • 包含 letter-spacing/word-spacing 的文本
  • 复杂的富文本(图片、链接混排)
  • 需要像素级精确的场景(建议实测验证)

安装

npm install @chenglou/pretext

总结

Pretext 通过将文本测量从「运行时查询 DOM」转变为「一次性测量 + 缓存算术」,为性能敏感的文本布局场景提供了可行方案。API 设计简洁,分层清晰,从基础的高度查询到细粒度的行迭代都有覆盖。


Further Reading