从一段会“跳”的文字说起
设想一个商品标题使用了这样的字体栈:font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;。在 Windows 上,PingFang SC 不存在,浏览器会回退到微软雅黑。两个字体的 x 高度(小写字母 x 的高度与字号之比)差异明显,导致标题中的拉丁字符、数字以及中文的视觉重心发生偏移。更麻烦的是,当网页字体通过 font-display: swap 延迟加载时,文本先以备用字体渲染,字体就绪后再切换,如果两者的 x 高度不一致,行高和布局会随之跳动,用户会看到文字“跳”了一下。
传统上,开发者用 JavaScript 检测字体加载状态,然后手动调整备用字体的 font-size 来补偿差异。这种做法需要维护字体度量数据,而且无法覆盖所有回退路径。CSS 的 font-size-adjust 属性正是为了解决这个问题而设计的:它允许你指定一个目标字体度量(默认是 x 高度与字号的比例,即 aspect value),浏览器会调整每个字体的实际渲染尺寸,使该度量达到指定值。这样,无论最终使用字体栈中的哪一个字体,文本的视觉尺寸都能保持一致。
x 高度与 aspect value:为什么小写字母决定视觉大小
字体的视觉大小并不直接等于 font-size 指定的数值。font-size 设置的是字体 em 框的高度,而 glyph 实际占据的空间取决于字体的度量。对于拉丁文字,小写字母的高度(x-height)是影响可读性的关键因素,尤其是在小字号下。两个字体即使 font-size 相同,如果 x 高度差异大,一个会显得“大而松”,另一个会显得“小而紧”。
x 高度与字号的比例称为 aspect value。例如,Times New Roman 的 aspect value 约为 0.45,Helvetica 约为 0.52,Verdana 约为 0.58。这个比例是字体设计的内在属性,不会随字号变化。font-size-adjust 的核心思想就是让备用字体的 aspect value 与首选字体对齐。
对于中文网页,情况更复杂。中文字符是方块字,其高度通常接近 em 框,但中文文本中常混有拉丁字母和数字,这些字符的视觉大小会显著影响整体排版。此外,中文字体的 x 高度与拉丁字体差异可能更大。例如,思源黑体(Noto Sans CJK)的拉丁部分 x 高度较高,而一些宋体字形的拉丁部分可能较小。当字体栈中同时包含中文字体和拉丁字体时,font-size-adjust 可以统一它们的视觉尺度,避免混排时拉丁字符显得过大或过小。
font-size-adjust 的计算机制
font-size-adjust 的语法支持多种取值。最基本的形式是 font-size-adjust: <number>,这个数字表示目标 aspect value。例如,font-size-adjust: 0.5 意味着希望所有字体的 x 高度都是字号的 50%。浏览器会为每个实际使用的字体计算一个调整系数:
调整后字号 = 原字号 × (目标 aspect value / 字体实际 aspect value)
以资料中的例子说明:如果首选字体 Verdana 的 aspect value 是 0.58,回退字体 Times New Roman 的是 0.45,设置 font-size-adjust: 0.58,当回退发生时,Times New Roman 的实际渲染字号会变为 16px × (0.58 / 0.45) ≈ 20.62px。这样,两种字体的 x 高度都约为 9.28px,视觉大小一致。
from-font 关键字则让浏览器自动从字体栈中第一个可用的字体读取其 aspect value,作为目标值。这避免了手动指定数字的麻烦,尤其适合字体栈经常变化的场景。CSS Fonts Module Level 4 还引入了 ex-height、cap-height、ch-width、ic-width、ic-height 等字体度量选项,允许你选择不同的度量基准。例如,cap-height 用于统一大写字母的高度,ic-width 和 ic-height 使用“水”字(U+6C34)的度量,专门针对中日韩文字。对于中文网页,ic-width 或 ic-height 可能比 ex-height 更有意义,因为它们直接对应汉字的方块尺寸。
值得注意的是,font-size-adjust 的调整是作用于每个实际渲染的字体,而不仅仅是备用字体。也就是说,如果首选字体可用,它也会被调整。因此,如果你设置的目标 aspect value 与首选字体的实际值不一致,首选字体也会被缩放。MDN 建议,此时最好直接调整 font-size,而不是使用 font-size-adjust。
对行高与布局的影响:一个贯穿全文的案例
为了具体说明,我们构造一个贯穿全文的场景:一个中文新闻网站的标题,字体栈为 font-family: 'Noto Sans SC', 'Source Han Sans SC', 'Microsoft YaHei', sans-serif;。在 macOS 上,Noto Sans SC 可用;在 Windows 上,会回退到微软雅黑。两种字体的 x 高度和行高度量不同,导致标题在不同平台上的高度和行数可能不同,影响页面布局的稳定性。
假设我们设置 font-size: 24px; line-height: 1.5;。正常情况下,行高为 36px。如果回退字体的 x 高度较大,即使字号相同,文字可能显得更挤,甚至需要更多行才能放下。font-size-adjust 通过调整字号来统一视觉大小,但行高是基于 font-size 计算的,因此调整字号会间接改变行高。
具体来说,line-height 是一个无单位值时,它乘以的是元素的 font-size。当 font-size-adjust 将回退字体的渲染字号从 24px 放大到 28px 时,行高也会从 36px 变为 42px。这可能导致标题占据的垂直空间变大,进而影响下方内容的布局。
因此,使用 font-size-adjust 时,必须考虑它对行高的连锁反应。如果希望行高保持不变,可能需要同时调整 line-height 的值,或者使用固定像素的行高。但固定行高又可能造成文字溢出。这是一个需要权衡的工程决策。
与 @font-face 的 size-adjust 描述符的配合
@font-face 规则中的 size-adjust 描述符可以调整整个字体的缩放比例,类似于 font-size-adjust 但作用域不同。size-adjust 作用于单个 @font-face 定义的字体,而 font-size-adjust 作用于整个元素的所有字体。两者可以配合使用。
例如,你可以在 @font-face 中为自定义字体设置 size-adjust: 90%,使其与系统字体在视觉上更接近。然后,在元素上使用 font-size-adjust: from-font,让浏览器从第一个可用字体(可能是自定义字体,也可能是回退字体)读取度量,并统一其他字体。
但需要注意,如果 @font-face 中已经通过 size-adjust 覆盖了字体度量,那么 font-size-adjust 的计算会使用覆盖后的度量。MDN 特别指出,当两者同时应用时,size-adjust 实际上不会产生额外效果,因为 font-size-adjust 会重新调整字号。这意味着,如果你已经用 size-adjust 优化了字体,再使用 font-size-adjust 可能会抵消它的效果。因此,最佳实践是:要么使用 size-adjust 来对齐字体度量,要么使用 font-size-adjust 来统一回退字体,避免两者叠加。
浏览器支持与兼容性边界
font-size-adjust 并非新属性,但长期以来支持不完整。根据 MDN 的“Baseline 2024”标记,该属性在 2024 年 7 月之后才在最新浏览器中广泛可用。具体而言,Chrome 131+、Firefox 131+、Safari 16.4+ 都完整支持数值和 from-font 值。Firefox 从 1.0 开始就支持数值,但 from-font 直到 131 才支持。
这意味着,如果你的用户仍在使用旧版浏览器(例如 Chrome 130 或更早),font-size-adjust 可能不被支持,或者只支持数值而不支持 from-font。在这种情况下,你需要提供回退方案:
- 使用
@supports检测font-size-adjust的支持情况,在不支持时使用 JavaScript 动态调整字号。 - 或者,仅对关键文本使用数值形式,因为数值形式在 Firefox 中一直支持,而 Chrome 和 Safari 在较新版本中也支持。
此外,from-font 是 CSS Fonts Level 4 的一部分,但资料中也有提到 Level 5,这可能表明规范仍在演进。实际使用时,应查阅最新的浏览器兼容性表。
常见失败模式与诊断路径
即使正确设置了 font-size-adjust,也可能遇到以下问题:
-
行高突变:如前所述,字号调整会改变行高,可能导致布局偏移。诊断方法是使用开发者工具检查元素的计算样式,对比不同字体下的
font-size和line-height。 -
字体度量不一致:同一字体的不同字重(如 bold)可能有不同的 x 高度。
font-size-adjust会分别调整每个字面,但如果目标值来自from-font,它只取第一个可用字体的度量,可能不适用于其他字重。 -
与
size-adjust冲突:如果同时使用size-adjust和font-size-adjust,后者会覆盖前者的效果,可能导致意外缩放。 -
性能影响:
font-size-adjust需要浏览器对每个字体进行度量计算,可能增加样式计算的开销,尤其是在大量元素上使用时。
诊断路径通常包括:使用 DevTools 的“Rendering”标签模拟不同字体,或者强制禁用某个字体来观察回退行为。还可以使用 Performance 面板检查样式计算时间。
替代方案与权衡
除了 font-size-adjust,还有其他方法可以缓解字体回退时的视觉不一致:
- 字体度量覆盖:在
@font-face中使用ascent-override、descent-override、line-gap-override描述符,可以覆盖字体的度量,使回退字体与首选字体在行盒上对齐。但这种方法不直接调整字号,而是调整行盒的尺寸,可能更适合控制行高。 - JavaScript 检测:通过
document.fonts.check()检测字体是否加载,然后动态调整样式。这种方法灵活但复杂,且可能导致闪烁。 - 使用系统字体栈:完全放弃自定义字体,使用
system-ui等系统字体,避免回退差异。但这样会失去品牌字体的一致性。
font-size-adjust 的优势在于纯 CSS、无需 JavaScript,且能自动适应字体栈中的任何字体。但它的缺点是会影响行高,需要仔细调整。相比之下,字体度量覆盖(如 size-adjust)更侧重于保持行盒一致,而 font-size-adjust 更侧重于视觉大小。
何时使用 font-size-adjust
font-size-adjust 最适合的场景是:文本包含大量小写拉丁字符(如英文、数字),且字体栈中不同字体的 x 高度差异明显。对于纯中文文本,由于汉字是方块字,x 高度的影响较小,但混排拉丁字符时仍然有用。
一个典型的应用是标题和导航栏,这些元素对视觉一致性要求高,且通常使用较大的字号,调整字号对行高的影响相对可控。
另一个场景是字体加载期间的视觉稳定性。配合 font-display: swap,当网页字体加载完成时,如果 font-size-adjust 已经统一了备用字体和网页字体的视觉大小,切换字体时就不会产生明显的跳动,从而减少 CLS(Cumulative Layout Shift)。
结论
font-size-adjust 通过调整字体的渲染字号,使备用字体的 x 高度(或其他度量)与首选字体保持一致,从而避免回退时的视觉突变。它适用于多字体回退栈,尤其是混排拉丁字符的中文网页。使用时需要关注它对行高的影响,以及与 @font-face 的 size-adjust 描述符的相互作用。浏览器支持在 2024 年后已趋于完整,但旧版浏览器仍需降级方案。
参考表格:不同字体度量选项的适用场景
| 字体度量 | 关键字 | 适用场景 | 典型使用示例 |
|---|---|---|---|
| x 高度 | ex-height(默认) | 统一小写字母的视觉大小,适合拉丁文本 | font-size-adjust: 0.5; |
| 大写高度 | cap-height | 统一大写字母的视觉大小,适合标题或全大写文本 | font-size-adjust: cap-height 0.7; |
| 零字符宽度 | ch-width | 统一等宽数字的宽度,适合表格数字 | font-size-adjust: ch-width 0.6; |
| CJK 表意文字宽度 | ic-width | 统一中文方块字的宽度,适合中文混排 | font-size-adjust: ic-width 1.0; |
| CJK 表意文字高度 | ic-height | 统一中文方块字的高度,适合中文文本 | font-size-adjust: ic-height 1.0; |
字体回退调整流程
flowchart TD
A[设置 font-family 字体栈] --> B{首选字体是否可用?}
B -- 是 --> C[使用首选字体渲染]
B -- 否 --> D[回退到下一个字体]
D --> E{是否设置了 font-size-adjust?}
E -- 是 --> F[读取目标字体度量(如 from-font 或数值)]
F --> G[计算调整系数 = 目标度量 / 实际字体度量]
G --> H[调整字号 = 原字号 × 调整系数]
H --> I[使用调整后的字号渲染文本]
E -- 否 --> J[使用原字号渲染文本]
C --> K[计算行高和布局]
I --> K
J --> K
流程图中,关键转折点在于 font-size-adjust 是否被设置。如果未设置,回退字体直接使用原字号,可能导致视觉大小不一致。如果设置了,浏览器会根据目标度量计算调整系数,并缩放字号。最终,行高和布局基于调整后的字号计算。