前端技术
#CSS#font-size-adjust#字体回退#x-height#排版

font-size-adjust 如何保持备用字体的视觉尺寸:从 x 高度到字体回退

当首选字体不可用而回退到备用字体时,文本的视觉大小可能突变。本文以中文网页的多字体回退栈为例,解释 font-size-adjust 如何通过调整字号使备用字体的 x 高度与首选字体一致,并分析其计算机制、对行高与布局的影响以及浏览器支持边界。

从一段会“跳”的文字说起

设想一个商品标题使用了这样的字体栈: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-heightcap-heightch-widthic-widthic-height 等字体度量选项,允许你选择不同的度量基准。例如,cap-height 用于统一大写字母的高度,ic-widthic-height 使用“水”字(U+6C34)的度量,专门针对中日韩文字。对于中文网页,ic-widthic-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,也可能遇到以下问题:

  1. 行高突变:如前所述,字号调整会改变行高,可能导致布局偏移。诊断方法是使用开发者工具检查元素的计算样式,对比不同字体下的 font-sizeline-height

  2. 字体度量不一致:同一字体的不同字重(如 bold)可能有不同的 x 高度。font-size-adjust 会分别调整每个字面,但如果目标值来自 from-font,它只取第一个可用字体的度量,可能不适用于其他字重。

  3. size-adjust 冲突:如果同时使用 size-adjustfont-size-adjust,后者会覆盖前者的效果,可能导致意外缩放。

  4. 性能影响font-size-adjust 需要浏览器对每个字体进行度量计算,可能增加样式计算的开销,尤其是在大量元素上使用时。

诊断路径通常包括:使用 DevTools 的“Rendering”标签模拟不同字体,或者强制禁用某个字体来观察回退行为。还可以使用 Performance 面板检查样式计算时间。

替代方案与权衡

除了 font-size-adjust,还有其他方法可以缓解字体回退时的视觉不一致:

  • 字体度量覆盖:在 @font-face 中使用 ascent-overridedescent-overrideline-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-facesize-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 是否被设置。如果未设置,回退字体直接使用原字号,可能导致视觉大小不一致。如果设置了,浏览器会根据目标度量计算调整系数,并缩放字号。最终,行高和布局基于调整后的字号计算。

资料来源

  1. MDN: font-size-adjust
  2. CSS Fonts Module Level 4 - font-size-adjust property
  3. [css] CSS font-size-adjust 属性是什么?如何保持不同字体的视觉大小一致? · Issue #2581 · hankliu62/interview · GitHub