⚡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

相关推荐
ZC跨境爬虫12 小时前
跟着 MDN 学JavaScript day_5:技能测试——变量实战
java·开发语言·前端·javascript
pan_junbiao12 小时前
Whistle 抓包工具的安装与使用
前端·测试工具·压力测试·抓包
Cory.眼12 小时前
前端调用后端接口全流程实战
前端·调用接口
牛栓柱12 小时前
【后端实战】用 Supabase + React/TS 零成本构建高并发 Multi-Agent 服务
前端·数据库·人工智能·后端·react.js·前端框架
木斯佳12 小时前
前端八股文面经大全:百度-Agent部门-前端一面(2026-06-04)·面经深度解析
前端
shmily麻瓜小菜鸡12 小时前
Bootstrap 4 常用工具类速查表
前端·javascript·bootstrap
CDN36012 小时前
【架构进阶】告别配置漂移!用 NodeNext + Workspace 打造优雅的 TypeScript Monorepo
前端·javascript·typescript
协享科技12 小时前
前端 SSE 流式响应处理实践:从接收、解析到渲染
前端·人工智能·程序人生·go·ai编程·sse
超人不会飞_Jay12 小时前
6.2前端笔记
前端·javascript·笔记
鹏大师运维12 小时前
统信UOS安装Subtitle Edit并使用Edge-TTS生成AI语音教程
linux·前端·人工智能·edge·麒麟·统信uos·ai语音