原理
Pretext 核心实现原理
传统方案的问题
在 Web 开发中,DOM 测量是性能杀手之一。
js
// 这些方法都会触发布局回流(Layout Reflow)
element.getBoundingClientRect() // 强制回流
element.offsetHeight // 强制回流
window.getComputedStyle(el).height // 强制回流1
2
3
4
2
3
4
为什么回流昂贵?
- 浏览器需要重新计算整个渲染树
- 这是同步的主线程操作
- 当 UI 每秒更新几十次时(比如输入框打字、虚拟滚动),累积卡顿明显
Pretext 的解决思路
核心思想:用 Canvas 测量替代 DOM 测量,完全绕开渲染树。
传统方式(回流):
用户输入 → DOM 更新 → 浏览器重新布局 → 读取高度 → 再次布局
Pretext 方式(零回流):
用户输入 → Canvas 预计算 → 纯算术运算 → 直接获取高度1
2
3
4
5
2
3
4
5
Canvas 文本测量原理
浏览器提供的 Canvas 2D API 可以测量文本宽度:
js
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
ctx.font = '16px Georgia'
const width = ctx.measureText('Hello').width // 不触发回流!1
2
3
4
2
3
4
Pretext 在初始化时使用 Canvas 测量所有字形的宽度,建立字形宽度表。之后计算文本总宽度时,只需要查表累加,纯算术运算,速度极快。
Unicode 字形簇处理
JavaScript 的 string.length 对复杂字符返回错误的值:
js
'🇨🇳'.length // 2(错误!实际是 1 个字符)
'👨👩👧👦'.length // 8(错误!实际是 1 个家庭 emoji)
'cafe\u0301'.length // 5(错误!é 是一个字符)1
2
3
2
3
Pretext 使用 Unicode Segmentation 标准正确切割"用户感知字符"(Grapheme Cluster),确保:
- Emoji 不会在中间被截断
- 组合字符(重音等)不会被错误分割
- 混合方向文本(阿拉伯文 + 中文 + 英文)正确处理
换行算法与粘合规则
粘合规则(Glue)
某些标点和符号在排版时应该和前后的词粘在一起:
js
// 这些应该和前面词不断开
"word ," // 逗号
"word ;" // 分号
"word !" // 感叹号
"(word)" // 括号内的词
// 这些应该和后面词不断开
"word (" // 左括号1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
Pretext 在 prepare() 阶段应用粘合规则,将应该粘在一起的字符标记为一个不可分割的单元。
换行算法
Pretext 支持多种换行策略:
Greedy(贪心):每行尽可能填词,到行尾断开。简单快速,但效果一般。
Knuth-Plass:全局优化算法,最小化"糟糕度"(badness)。效果最好,但计算量较大。
Knuth-Plass 核心概念:
- badness 函数:衡量某一行的不完美程度。偏离最优宽度的行会有更高的 badness 值。
- 总糟糕度:所有行的 badness 之和。算法通过动态规划寻找总糟糕度最小的断行方案。
- 河流(rivers):CSS justify 在窄列下容易产生词间空白形成的"河流",Knuth-Plass 能有效消除。
性能优化
prepare() vs layout()
ts
// prepare() 是"冷路径",做一次性工作(Canvas 测量)
// 应该在初始化时或文本变化时调用
const prepared = prepare(text, font) // 约 19ms / 500 条
// layout() 是"热路径",纯算术运算
// 可以在每帧调用,不触发 DOM
const { height } = layout(prepared, width, lineHeight) // 约 0.09ms / 500 条1
2
3
4
5
6
7
2
3
4
5
6
7
批量处理
如果有多条文本需要测量,先批量 prepare(),再分别 layout():
ts
// 推荐:一次性 prepare
const prepared = texts.map(t => prepare(t, font))
// 然后 layout 可以反复调用
prepared.forEach(p => {
const { height } = layout(p, width, lineHeight)
})1
2
3
4
5
6
7
2
3
4
5
6
7
总结
| 概念 | 说明 |
|---|---|
| DOM 回流 | 读取 DOM 尺寸时触发,昂贵 |
| Canvas 测量 | measureText() 不触发回流 |
| Grapheme Cluster | 用户感知的字符边界 |
| 粘合规则 | 标点与词应作为一个单元 |
| Knuth-Plass | 全局优化的断行算法 |
| prepare() | 一次性预处理(冷路径) |
| layout() | 纯算术计算(热路径) |