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

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

为什么需要 Pretext?

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

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

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

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

核心 API

1. prepare + layout --- 快速测量

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

typescript 复制代码
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)  // 总行数
typescript 复制代码
// 输出示例
// { height: 48, lineCount: 2 }

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

2. prepareWithSegments + layoutWithLines --- 获取行详情

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

typescript 复制代码
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)`)
})
typescript 复制代码
// 输出示例
// Line 1: "Hello World!" (81px)
// Line 2: "This is" (42px)
// Line 3: "Pretext." (60px)

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

3. walkLineRanges --- 回调遍历

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

typescript 复制代码
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 --- 迭代器模式

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

typescript 复制代码
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' 选项

保留换行和缩进。

typescript 复制代码
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 集成示例

typescript 复制代码
<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 的文本
  • 复杂的富文本(图片、链接混排)
  • 需要像素级精确的场景(建议实测验证)

安装

bash 复制代码
npm install @chenglou/pretext

总结

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


Further Reading

相关推荐
不会聊天真君6471 小时前
JavaScript基础语法(Web前端开发笔记第三期)
前端·javascript·笔记
IT_陈寒1 小时前
SpringBoot自动配置这破玩意儿又坑我一次
前端·人工智能·后端
妖精的羽翼2 小时前
前端(Vue)→ 全栈 + AI 应用开发
前端
码路飞2 小时前
玩了一圈 AI 编程工具,Background Agent 才是让我真正震撼的东西
前端·javascript
UCloud_TShare2 小时前
优刻得发布云搜索服务CSS:面向AI时代的企业级搜索基础设施
前端·css·人工智能
木斯佳2 小时前
前端八股文面经大全:字节暑期前端一面(2026-04-21)·面经深度解析
前端·面试·校招·面经·实习
Jolyne_3 小时前
前端从0开始的LangChain学习(一)
前端·langchain
掘金一周3 小时前
掘友们,一人说一个你买过夯到爆的东西 | 沸点周刊 4.23
前端·人工智能·后端
Developer_Niuge3 小时前
告别翻不动的 1000+ 书签:开源 Chrome / Edge 浏览器书签管理插件 Smart Bookmark 0.2 发布
前端·后端