用 JavaScript 搞定高级排版,这个库太强了
用 JavaScript 搞定高级排版,这个库太强了
昨天刷到个叫 pretext 的库,玩了一整天。
说实话,一开始我是怀疑的:“不就是测量文本高度吗,能有多花哨?”
结果被打脸了。这个库不仅能测量文本,还能做动态排版、瀑布流、甚至 ASCII 艺术生成。
最重要的是:完全不用 DOM 测量,零 reflow,性能炸裂。
它解决什么问题
传统做法有多坑
以前要测量文本高度,我们都是这样:
const div = document.createElement('div')
div.textContent = text
div.style.font = '16px Inter'
div.style.width = '320px'
document.body.appendChild(div)
const height = div.offsetHeight // 💥 触发 reflow!
document.body.removeChild(div)
每次测一下就要触发一次 layout reflow,一个虚拟列表要测几百个,浏览器就被卡死了。
pretext 的黑科技
pretext 用浏览器的 font engine 作为"基准",实现了自己的文本测量逻辑,完全不需要 DOM 操作:
import { prepare, layout } from '@chenglou/pretext'
// 一次性准备(只测一次)
const prepared = prepare('AGI 春天到了. بدأت الرحلة 🚀', '16px Inter')
// 布局(纯数学计算,超快!)
const { height, lineCount } = layout(prepared, 320, 20)
// height: 60, lineCount: 3
prepare() 做了一次性工作:归一化空格、分词、测量段宽,返回一个 opaque handle。
layout() 是热点路径:纯算术运算,复用缓存的宽度,不测 DOM。
💬 炫酷 Demo 1:聊天气泡自动收缩
痛点
聊天应用里,消息气泡总是要么太宽(浪费空间),要么换行太多(难看)。
效果对比:
传统 CSS 方式:
- 短消息:气泡太宽,左边留大块空白
- 长消息:换行太多,看着累赘
pretext 方式:
- 短消息:气泡紧凑,刚好容纳文本
- 长消息:自动换行,每行充分利用空间
pretext 的方案
用 walkLineRanges() 找到每一行的实际宽度,然后取最大值,就是最紧凑的容器宽度:
import { prepareWithSegments, walkLineRanges } from '@chenglou/pretext'
const prepared = prepareWithSegments(message, '16px Inter')
let maxWidth = 0
walkLineRanges(prepared, 400, line => {
if (line.width > maxWidth) maxWidth = line.width
})
// maxWidth 就是刚好能装下所有文本的最小宽度
// 消息气泡宽度 = Math.min(maxWidth, 屏幕宽度的70%)
效算
- 长消息:“这个 pretext 库太强了” → 气泡宽度 240px(刚好装下)
- 短消息:“你好!” → 气泡宽度 60px(不浪费空间)
- 换行控制:lineCount 和 maxWidth 都可控
这叫 multiline shrink-wrap,Web 原生没有这个能力,pretext 补上了。
📰 炫酷 Demo 2:文字绕图排版
效果
像杂志排版那样,文字绕着图片流动,图片在左边时右边文字窄一些,图片结束后恢复正常宽度。
代码
import { prepareWithSegments, layoutNextLineRange, materializeLineRange } from '@chenglou/pretext'
const prepared = prepareWithSegments(article, '17px Inter')
let cursor = { segmentIndex: 0, graphemeIndex: 0 }
let y = 0
const imageHeight = 140
const imageWidth = 120
const columnWidth = 320
while (true) {
// 图片左边时,文字区域变窄
const width = y < imageHeight ? columnWidth - imageWidth : columnWidth
const range = layoutNextLineRange(prepared, cursor, width)
if (range === null) break
const line = materializeLineRange(prepared, range)
// 渲染到 Canvas 或 SVG
ctx.fillText(line.text, width < columnWidth ? imageWidth + 16 : 0, y)
cursor = range.end
y += 26
}
炫酷在哪里
- 完全在 JavaScript 层计算,不用 CSS float hack
- 可以渲染到 Canvas、SVG,甚至 WebGL
- 图片位置可以动态调整,实时重字
- 像专业杂志排版一样,文字流畅绕过图片
🧱 炫酷 Demo 3:瀑布流虚拟化
传统瀑布流的痛点
要实现瀑布流虚拟化,必须知道每个卡片的高度。传统做法:
- 渲染 DOM
- 测量
offsetHeight - 用测高度来计算位置
- 卡顿
pretext 的方案
卡片高度预先算好,虚拟化时直接用:
import { prepare, layout } from '@chenglou/pretext'
// 批量计算所有卡片的高度
const cardHeights = cards.map(card => {
const prepared = prepare(card.content, '16px Inter')
const { height } = layout(prepared, 200, 24)
return height + 100 // 文字高度 + 图片高度 + padding
})
// 虚拟化滚动时,只用查表
const visibleCards = getVisibleCards(scrollTop, cardHeights)
性能提升
| 指标 | 传统做法 | pretext | 提升 |
|---|---|---|---|
| 首屏渲染 | 1200ms | 80ms | 15倍 |
| 滚动帧率 | 30fps | 60fps | 2倍 |
| 内存占用 | 高 | 低 70% | - |
| Reflow | 数百次 | 0次 | - |
🎨 炫酷 Demo 4:富文本排版
痛点
一段文字里有不同样式的片段:粗体、@提及、链接、标签。
CSS 能做,但想控制每个 fragment 的精确位置和宽度,就得靠 JavaScript。
pretext 的 rich-inline 模块
import { prepareRichInline, walkRichInlineLineRanges, materializeRichInlineLineRange } from '@chenglou/pretext/rich-inline'
const prepared = prepareRichInline([
{ text: 'Ship ', font: '500 17px Inter' },
{ text: '@maya', font: '700 12px Inter', break: 'never', extraWidth: 22 }, // 提及,不换行
{ text: "'s rich-note", font: '500 17px Inter' },
])
walkRichInlineLineRanges(prepared, 320, range => {
const line = materializeRichInlineLineRange(prepared, range)
// line.fragments 里每个 fragment 都有:
// - text: 文本内容
// - font: 字体
// - gapBefore: 前置空格宽度
// - cursors: 起止位置
})
效果
@maya标签整体不换行(break: 'never')- 标签宽度包含 pill 装饰(
extraWidth: 22) - 换行时边界空格正确处理(不像 CSS 那样容易出错)
- 多种字体混合,精确测量
🎮 炫酷 Demo 5:动态 ASCII 艺术
效果
用等宽字体和比例字体分别渲染 ASCII 艺术,对比效果。
为什么炫酷
pretext 能处理:
- Grapheme 级别:不是 char,是真正的"字"(emoji 算一个)
- 所有语言:中文、阿拉伯语、希伯来语,统统支持
- 组合字符:emoji 修饰符、变体选择器
这意味着你可以用 JavaScript 做任何 Unicode 文本的精确排版。
性能到底有多快
官方数据
- prepare(): 测 1000 个 100 字的字符串,耗时 ~150ms
- layout(): 同样数据,纯计算,耗时 <1ms
对比
| 方法 | 1000次测高 | 内存 | reflow |
|---|---|---|---|
| DOM 测量 | 800ms | 高 | 1000次 |
| pretext | 150ms | 低 | 0次 |
实际场景
虚拟列表里,用户滚动一屏(20个 item):
- DOM 测量:触发 20 次 reflow,200ms
- pretext:全用缓存,<1ms
支持的渲染目标
不只是 DOM
pretext 计算完布局后,可以渲染到:
- Canvas: 适合游戏、数据可视化
- SVG: 适合高质量输出、打印
- WebGL: 适合 3D 混合场景
- Server-side: 未来支持,可以在 Node.js 里预计算
Canvas 渲染示例
import { prepareWithSegments, layoutWithLines } from '@chenglou/pretext'
const prepared = prepareWithSegments(text, '18px "Helvetica Neue"')
const { lines } = layoutWithLines(prepared, 320, 26)
const ctx = canvas.getContext('2d')
ctx.font = '18px "Helvetica Neue"'
for (let i = 0; i < lines.length; i++) {
ctx.fillText(lines[i].text, 0, i * 26)
}
适用场景
适合用 pretext
- 虚拟列表/滚动: 需要提前算高度
- 自定义布局: masonry、瀑布流、异形容器
- Canvas/SVG 渲染: 不能用 DOM
- 复杂富文本: 多字体、多样式混合
- 多语言支持: 阿拉伯语、中文等混合
- AI 辅助开发: 需要浏览器无关的文本测量
不适合
- 简单的单行文本测量(CSS line-height 够用)
- 静态页面,不需要动态计算
- 只需要浏览器原生能力覆盖的场景
写在最后
说实话,pretext 最大的价值不是"能做多少事情",而是**“不做 DOM 测量”**。
浏览器的 layout reflow 是最贵的操作之一,能避开就避开。
现在有了 pretext,我们在 JavaScript 层就能搞定大部分文本布局问题,而且还能渲染到 Canvas、SVG,甚至未来能在服务器端预计算。
这对于:
- 想做高级 UI 的前端
- 需要 Canvas 渲染的游戏开发者
- 做 AI 辅助开发的工具作者
都是福音。
npm 地址:@chenglou/pretext
下一步打算用 pretext 做个虚拟聊天列表,感兴趣可以关注一下。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)