从一行标题说起
设想一个商品详情页的标题区,文案是“2026 春季新款轻量防风夹克 男女同款”。容器宽度固定为 320px,默认的 text-wrap: wrap 会让浏览器在空格处换行,结果可能是第一行“2026 春季新款轻量防风夹克”,第二行“男女同款”。第一行几乎占满整行,第二行只有四个字,视觉上头重脚轻。设计师希望两行长度接近,于是手动在“夹克”后插入 <br>,或者用 JavaScript 测量文本宽度后动态插入换行。这些方案能解决问题,但各有代价:手动换行在容器宽度变化或字体缩放时失效,JS 测量则引入主线程计算和布局抖动。
CSS Text Module Level 4 引入了 text-wrap: balance,让浏览器自动平衡多行文本的宽度。这个属性不是简单的“居中”或“两端对齐”,它改变的是浏览器决定软换行位置的方式。本文以标题排版为贯穿场景,解释 balance 的布局计算机制、性能开销、适用边界,以及它与 text-wrap: pretty 的差异。
软换行与 text-wrap 属性家族
文本在块级元素内流动时,换行分为两类:由用户控制的强制换行(如 <br>)和由浏览器根据容器宽度自动决定的软换行。默认情况下,浏览器采用贪心算法:从左到右填充字符,直到下一个单词放不下时换行。这种算法速度极快,但不会考虑各行的长度是否均衡。
text-wrap 是 CSS Text Module Level 4 中定义的一个简写属性,它由 text-wrap-mode 和 text-wrap-style 两个子属性组成。text-wrap-mode 决定是否换行(wrap 或 nowrap),text-wrap-style 决定换行时采用何种策略。balance 是 text-wrap-style 的一个值,它告诉浏览器:在允许的换行点中,选择一组让各行宽度尽可能接近的换行位置。
MDN 文档指出,balance 值“以最佳方式平衡每行的字符数,提升布局质量和可读性”。这里的“平衡”不是简单的等宽,而是让各行的“填充度”(即行宽与容器宽度的比值)尽量接近。对于标题这种短文本,平衡后的行宽更均匀,视觉重心更稳。
balance 的布局计算机制
text-wrap: balance 的核心算法是“基于最大宽度的迭代”。浏览器首先用默认的贪心算法对文本进行一次布局,得到总行数 N。然后,它尝试寻找一个目标行宽 W,使得当以 W 作为换行阈值时,文本恰好能分成 N 行,且每行的实际宽度都不超过 W。这个 W 通常小于容器宽度,因为如果某一行接近容器宽度,其他行就很难填满。
具体过程可以这样理解:
- 用贪心算法布局,得到行数
N。 - 设定目标行宽
W的初始值为容器宽度。 - 尝试用
W作为换行宽度重新布局。如果得到的行数大于N,说明W太小,需要增大;如果行数小于N,说明W太大,可以减小。 - 通过二分搜索或线性扫描,找到满足“恰好
N行”的最大W。 - 用这个
W确定最终的换行点。
这个过程中,浏览器需要多次执行文本布局。每次布局都要测量每个单词的宽度、检查换行点,因此计算成本远高于单次贪心布局。为了控制开销,规范要求浏览器限制 balance 影响的行数。MDN 明确提到:Chromium 限制为六行或更少,Firefox 限制为十行或更少。这意味着 balance 只对短文本有效,长段落使用它不会生效,浏览器会回退到普通换行。
下面用流程图展示 balance 的决策过程,以标题“2026 春季新款轻量防风夹克 男女同款”为例:
flowchart TD
A[文本进入布局] --> B{行数是否 <= 限制?}
B -- 否 --> C[使用普通换行 wrap]
B -- 是 --> D[贪心布局得到行数 N]
D --> E[设置目标宽度 W = 容器宽度]
E --> F[用 W 重新布局]
F --> G{行数 == N?}
G -- 否 --> H[调整 W 并重新布局]
H --> F
G -- 是 --> I[采用当前换行点]
C --> I
图中关键转折点是“行数是否 <= 限制”。如果标题超过六行(Chromium),balance 直接失效,退化为普通换行。对于我们的标题,通常只有两行,因此会进入迭代过程。
手动换行与 JS 测量的替代方案
在 balance 出现之前,前端工程师常用两种方法实现标题平衡:手动插入 <br> 或使用 JavaScript 测量。
手动换行是最直接的方式。设计师根据设计稿的宽度,在合适的位置插入换行标签。例如,将标题写成“2026 春季新款轻量防风夹克
男女同款”,强制两行。这种方法的优点是零运行时开销,缺点是完全静态:容器宽度变化(响应式布局)、字体大小调整、用户缩放浏览器时,固定换行点可能不再合适。例如,在 375px 宽的屏幕上,第一行可能还有剩余空间,但 <br> 强制换行导致第二行只有四个字,反而更不平衡。
JS 测量方案则动态计算。常见做法是:隐藏元素,逐词拼接文本,测量每次拼接的宽度,当超过容器宽度时插入换行。这能适应容器变化,但代价高昂:每次测量都会触发布局(如果读取 offsetWidth),而且需要监听窗口 resize 事件重新计算。在低端设备上,频繁的布局计算可能导致卡顿。此外,JS 方案必须处理字体加载、换行规则(如中英文混排)等细节,实现复杂度高。
balance 的价值在于把这项工作交给浏览器,由渲染引擎在布局阶段完成,无需开发者维护换行点,也不占用 JavaScript 主线程。但代价是布局计算次数增加,这正是性能权衡的核心。
性能开销:布局次数与主线程负担
balance 的性能开销主要来自多次布局。每次尝试不同的目标宽度 W,浏览器都要重新计算文本的换行位置。对于短文本,行数少,迭代次数有限;但对于长文本,如果行数超过限制,浏览器会直接放弃,因此不会产生额外开销。MDN 指出:“由于浏览器限制了受此属性影响的行数,该值对性能的影响可以忽略不计。”这句话需要正确理解:它指的是在受支持的行数范围内,额外布局的次数是有限的,而不是说完全没有开销。
实际开销取决于文本长度和迭代次数。假设标题有两行,浏览器可能尝试 3~5 次布局就能找到合适的 W。每次布局涉及测量单词宽度、比较换行点,对于几十个字符的标题,耗时在微秒级。但如果页面中有大量标题都使用 balance,累计开销就会显现。例如,一个商品列表页有 50 个标题,每个标题多 4 次布局,总共多 200 次布局,可能使首屏渲染时间增加几十毫秒。
另一个隐藏成本是布局抖动。如果 balance 的布局过程发生在样式计算之后、绘制之前,它不会导致强制同步布局,但会延长布局阶段的总时间。在性能敏感的场景(如滚动时动态添加内容),应谨慎使用。
balance 与 pretty 的差异
text-wrap 还提供了 pretty 值,它与 balance 经常被混淆。pretty 的行为与默认的 wrap 相同,但浏览器会使用更慢的算法,优先考虑排版质量而非速度。典型应用是正文段落,目标是减少孤行(orphan)——即段落最后一行只有一个单词的情况。
balance 和 pretty 的核心区别在于:
- 目标不同:balance 追求各行宽度均衡,pretty 追求避免孤行和优化断行质量。
- 适用文本:balance 只适用于短文本(标题、引文),pretty 适用于长文本(正文)。
- 性能影响:balance 因行数限制,额外开销可控;pretty 没有行数限制,但算法更慢,对长文本性能影响明显。MDN 明确警告“pretty 对性能有负面影响,应仅在布局比速度更重要时用于长文本块”。
下表从多个维度对比 balance、pretty 和默认 wrap:
| 维度 | wrap(默认) | balance | pretty |
|---|---|---|---|
| 换行策略 | 贪心:填满一行再换 | 平衡各行宽度 | 优化断行质量,减少孤行 |
| 适用文本 | 任意 | 短文本(标题、引文) | 长文本(正文) |
| 行数限制 | 无 | Chromium 6 行,Firefox 10 行 | 无 |
| 布局开销 | 低(单次布局) | 中(多次迭代布局) | 高(慢速算法) |
| 性能建议 | 默认 | 标题等短文本 | 仅当排版优先时使用 |
这个表格帮助决策:如果目标是标题均衡,选 balance;如果是正文排版质量,选 pretty;如果性能优先,保持默认 wrap。
适用边界与失败模式
balance 并非万能,它有明确的适用边界和失效条件。
行数限制是最重要的边界。当文本超过浏览器限制的行数时,balance 不会生效,而是回退到普通换行。这可能导致开发者预期落空:一个长标题在宽屏上可能显示为两行,但在窄屏上变成三行,超出限制后平衡效果消失。
容器宽度变化也会影响平衡效果。balance 是在布局时根据当前容器宽度计算的,如果容器宽度改变(如响应式断点),浏览器会重新布局,平衡结果随之更新。但如果宽度改变后行数超过限制,平衡会失效。
内容动态变化时,balance 需要重新计算。对于 contenteditable 元素,用户编辑时每一行都可能重新平衡,这会影响编辑体验。stable 值可以保持编辑行之前的行稳定,但 balance 本身没有这个保证。
性能退化的典型场景是大量标题同时使用 balance。例如,博客列表页有 100 个标题,每个标题多 3 次布局,总布局次数增加 300 次。在低端移动设备上,这可能使首屏渲染时间明显增加。此时应权衡:是否所有标题都需要平衡?或者只对首屏可见的标题应用 balance,其余使用默认 wrap。
不适用场景包括:单行文本(balance 无意义)、长段落(超出行数限制)、性能敏感的滚动容器(频繁布局)。
可观测信号与诊断路径
要判断 balance 是否按预期工作,以及是否带来性能问题,可以观察以下信号:
- 布局时间:使用 Performance 面板记录 Layout 阶段耗时。如果启用 balance 后布局时间显著增加,说明迭代次数过多或元素数量过大。
- 行数变化:在 DevTools 中检查元素的计算样式,确认
text-wrap-style是否为balance。如果文本超过限制,浏览器可能将其计算为auto。 - 视觉检查:观察标题各行宽度是否接近。如果某行明显过长或过短,可能是容器宽度变化导致平衡失效。
诊断路径:首先确认浏览器支持 balance(MDN 标注为 Baseline 2024,即 2024 年 3 月起广泛可用)。然后检查元素的行数是否在限制内。如果行数超限,考虑缩短文本或调整容器宽度。如果性能有问题,使用 Performance 面板定位布局热点,并考虑减少 balance 的使用范围。
权衡与决策建议
在标题排版场景中,balance 提供了一种声明式的解决方案,避免了手动换行的脆弱性和 JS 测量的复杂性。它的收益是视觉质量提升和开发效率提高,成本是布局计算次数增加。
与手动换行相比,balance 的优势在于自适应容器宽度和字体变化,劣势是可能引入额外的布局开销。与 JS 测量相比,balance 不占用主线程,也不依赖 JavaScript 执行时机,但无法处理复杂的换行逻辑(如自定义断行规则)。
实际项目中,建议遵循以下原则:
- 标题、引文、卡片标题等短文本,优先使用
text-wrap: balance。 - 正文段落,如果排版质量要求高,考虑
text-wrap: pretty,但需接受性能代价。 - 性能敏感页面(如电商列表页),只对首屏可见的标题使用 balance,或通过 CSS 类控制。
- 始终测试不同容器宽度下的表现,确保行数在限制内。
balance 是 CSS 排版能力的一次重要补充,它把文本平衡从“人工干预”变为“浏览器原生能力”。理解其计算机制和性能边界,才能在实际项目中做出正确的工程决策。