TypeScript · MIT License · v0.0.3

Pretext
纯 JS 文本测量与布局引擎

告别 getBoundingClientRect 和 DOM 回流。Pretext 用纯算术实现多行文本的精确高度计算与行布局,支持全球所有语言,可渲染至 DOM、Canvas、SVG。

27K+ Stars
1.2K+ Forks
TypeScript Language
MIT License
text-layout-preview
height: 120px lines: 5 0.09ms

解决 Web 文本布局的核心痛点

DOM 测量触发的布局回流是浏览器最昂贵的操作之一。Pretext 从根本上绕过了这个问题。

传统方式:DOM 回流陷阱

使用 getBoundingClientRectoffsetHeight 测量文本高度时,浏览器必须同步触发布局回流。当多个组件独立测量文本,每次测量都会引发整个文档的回流。对于 500 个文本块,这可能带来 30ms+/帧 的性能开销。

Pretext:纯算术零回流

Pretext 采用两阶段策略:prepare() 一次性分析并缓存文本段宽度;layout() 使用纯算术遍历缓存的宽度数据计算高度。在 resize 时只需重跑 layout(),单次调用仅需约 0.0002ms

为什么开发者选择 Pretext

精心设计的 API,极致的性能,以及对全球语言的全面支持。

零 DOM 回流

完全绕过 getBoundingClientRect 和 offsetHeight,通过 Canvas measureText 预计算 + 纯算术布局,从根本上消除布局回流。

🚀

极致性能

prepare() 约 19ms(500 文本批量),layout() 仅 0.09ms。resize 热路径零 DOM 读取、零 Canvas 调用、零字符串操作。

🌐

全球语言支持

基于 Intl.Segmenter 实现 CJK、泰语、阿拉伯语等所有语言的正确分词。支持 Emoji、ZWJ 序列和混合双向文本。

🎨

多渲染目标

不仅限于 DOM,还可渲染至 Canvas、SVG、WebGL。layoutNextLine() 支持逐行变宽布局,实现文字环绕图片等复杂排版。

📐

精确的行布局

layoutWithLines() 返回完整行信息;walkLineRanges() 提供零字符串开销的行宽扫描,支持 shrink-wrap 和平衡文本布局。

📝

Pre-wrap 模式

支持 textarea 风格的文本保留:普通空格、Tab 制表符、换行符均按原样保留,适配输入框和编辑器场景。

快到无法感知的布局计算

~30ms+/帧
~19ms
~0.09ms

两阶段架构的威力

prepare() 阶段:文本标准化、分段、glue 规则应用、Canvas 段宽测量。这是一次性的预计算工作。

layout() 阶段:遍历缓存的段宽度,纯算术计算行数和高度。这是 resize 热路径——零 DOM、零 Canvas、零分配。

0.0002ms 单文本 layout() 耗时
0 layout() 中 DOM 操作
100% 纯算术计算

简洁直观的 API 设计

两行代码即可获取精确的文本高度,无需任何 DOM 操作。

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

// 一次性预计算:分析文本、分段、测量段宽度
const prepared = prepare('AGI 春天到了. بدأت الرحلة 🚀', '16px Inter')

// 热路径:纯算术计算高度,resize 时只需重跑此行
const { height, lineCount } = layout(prepared, textWidth, 20)

// 🎉 就这么简单!零 DOM 回流!
import { prepareWithSegments, layoutWithLines } from '@chenglou/pretext'

const prepared = prepareWithSegments(
  'AGI 春天到了. بدأت الرحلة 🚀',
  '18px "Helvetica Neue"'
)

// 获取完整的行信息
const { lines } = layoutWithLines(prepared, 320, 26)

// 渲染到 Canvas
for (let i = 0; i < lines.length; i++) {
  ctx.fillText(lines[i].text, 0, i * 26)
}
import { prepareWithSegments, walkLineRanges } from '@chenglou/pretext'

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

// 零字符串分配的行宽扫描
let maxW = 0
walkLineRanges(prepared, 320, line => {
  if (line.width > maxW) maxW = line.width
})

// maxW 即最宽行宽——精确的 shrink-wrap 容器宽度!
// 这种"多行 shrink wrap"一直是 Web 中缺失的能力
import { prepareWithSegments, layoutNextLine } from '@chenglou/pretext'

const prepared = prepareWithSegments(text, '16px Inter')
let cursor = { segmentIndex: 0, graphemeIndex: 0 }
let y = 0

// 文字环绕浮动图片:图片旁的行更窄
while (true) {
  const width = y < image.bottom
    ? columnWidth - image.width
    : columnWidth
  const line = layoutNextLine(prepared, cursor, width)
  if (line === null) break
  ctx.fillText(line.text, 0, y)
  cursor = line.end
  y += 26
}

释放 Web UI 的全部潜能

Pretext 提供的精确文本高度是许多高级 UI 模式的关键拼图。

📱

精确虚拟滚动

无需猜测或缓存的高度预测。在文本加载前就知道每个条目的精确高度,实现真正的无抖动虚拟列表。

🧱

Masonry 瀑布流布局

用 JavaScript 驱动的高级布局:瀑布流、Flexbox 模拟、自定义文本排版。告别 CSS hack,用数学精确控制。

🤖

AI 辅助 UI 验证

在开发阶段验证按钮标签是否溢出到下一行,无需启动浏览器。特别适合 AI 生成的 UI 组件验证。

📌

消除布局偏移

新文本加载时精确重新锚定滚动位置,防止 CLS(Cumulative Layout Shift)。提升用户体验和 Core Web Vitals。

全球语言无死角覆盖

基于浏览器原生 Intl.Segmenter 和 Canvas 字体引擎,Pretext 正确处理所有文字系统

English 中文 日本語 한국어 العربية ไทย हिन्दी Emoji 🚀🎉 Mixed Bidi ភាសាខ្មែរ မြန်မာ

几分钟即可集成到你的项目

通过 npm 安装即用,或克隆仓库体验完整的演示和工具链。

NPM 安装

推荐
$ npm install @chenglou/pretext

# 在项目中直接导入使用
import { prepare, layout } from '@chenglou/pretext'

const p = prepare('Hello World', '16px Inter')
const { height } = layout(p, 300, 20)

源码开发

开发者
# 克隆并启动开发服务器
$ git clone https://github.com/chenglou/pretext.git
$ cd pretext
$ bun install
$ bun start

# 打开 http://localhost:3000/demos
# 查看气泡 shrinkwrap、动态布局等演示

类型检查 & 测试

质量
# TypeScript 类型检查 + 代码质量
$ bun run check

# 运行不变量测试套件
$ bun test

# 浏览器准确性校验
$ bun run accuracy-check

构建 & 发布

发布
# 构建 ESM 发布包到 dist/
$ bun run build:package

# 打包烟雾测试
$ bun run package-smoke-test

# 静态演示站构建
$ bun run site:build

分层设计的 API 体系

从快速高度计算到完全自定义的逐行布局,按需选择合适的 API 层级。

快速路径
prepare()
prepare(text, font, options?) → PreparedText

一次性文本分析 + 测量。标准化空白、分段、应用 glue 规则、Canvas 测量段宽、返回不透明句柄。

快速路径
layout()
layout(prepared, maxWidth, lineHeight) → { height, lineCount }

resize 热路径。纯算术遍历缓存宽度,返回文本高度和行数。零 DOM、零 Canvas、零分配。

富布局
layoutWithLines()
layoutWithLines(prepared, maxWidth, lineHeight) → { lines[] }

返回完整的行数组,每行包含 text、width、start/end 游标。适合 Canvas/SVG 逐行渲染。

富布局
walkLineRanges()
walkLineRanges(prepared, maxWidth, onLine) → number

零字符串分配的行宽回调。适合二分搜索容器宽度、shrink-wrap 和平衡文本布局。

富布局
layoutNextLine()
layoutNextLine(prepared, start, maxWidth) → LayoutLine | null

迭代器风格 API,逐行布局并可为每行指定不同宽度。实现文字环绕、多栏流式排版。

工具
clearCache() / setLocale()
clearCache() · setLocale(locale?)

clearCache() 释放内部缓存;setLocale() 设置分词器语言环境,不影响已有的 prepare 结果。

需要了解的限制条件

Pretext 目标是覆盖常见的 App 文本场景,以下是当前的 CSS 目标配置。

📋

默认目标 CSS:white-space: normalword-break: normaloverflow-wrap: break-wordline-break: auto

📝

传入 { whiteSpace: 'pre-wrap' } 可保留空格、Tab 和换行符。Tab 遵循默认 tab-size: 8 浏览器标准。

system-ui 字体在 macOS 上不安全——Canvas 和 DOM 可能解析为不同字体。请使用具名字体如 Inter、Helvetica 等。

📐

由于目标包含 overflow-wrap: break-word,极窄宽度下可能在字内断行,但仅在字素边界处断开。