1997年,Internet Explorer 4成为第一个支持@font-face的浏览器。当其他浏览器还在用系统字体渲染网页时,IE给开发者打开了通往排版自由的大门——但伴随着一个问题:如果字体还没加载完,用户会看到什么?

答案是FOUT(Flash of Unstyled Text):先用系统字体显示,字体加载完再替换。这个设计在当年看起来理所当然,却在接下来的二十年里演变成了一场关于性能、美学与用户体验的三方博弈。

两条分歧的道路

2008年,Safari 3.2加入@font-face支持时,做出了一个完全不同的选择:在字体加载完成前,文本保持不可见。这种策略被称为FOIT(Flash of Invisible Text)。

浏览器阵营就此分裂:

浏览器 策略 行为
IE/Edge FOUT 立即显示后备字体
Safari FOIT 无限期等待(直到网络超时)
Chrome/Firefox FOIT + 3秒超时 等待3秒后显示后备字体

Zach Leatherman在2017年整理的FOIT/FOUT历史时间线显示,这种分裂持续了八年之久。2011年,Firefox 4率先引入了3秒超时——第一个"有条件的FOIT"。2014年,Chrome 35跟进。直到2016年,Safari 10才终于加入超时机制——距离它首次引入FOIT已经过去了整整八年。

问题的核心不在于哪种策略"更好",而在于开发者没有选择权。一个想用FOUT的网站在Safari上会被强制FOIT,一个想用FOIT的网站在IE上会被强制FOUT。这种不一致性持续困扰着前端开发者,直到2017年font-display属性的出现。

font-display:时间轴上的五个选择

CSS Fonts Module Level 4定义的font-display属性,本质上是在控制字体下载生命周期的三个阶段:

  1. 阻塞期(Block Period):如果字体未加载,使用不可见的后备字体
  2. 交换期(Swap Period):如果字体未加载,使用可见的后备字体,字体加载后立即替换
  3. 失败期(Failure Period):字体加载失败,使用后备字体

font-display的五个值实际上是这三个阶段的不同组合:

阻塞期 交换期 典型场景
auto 浏览器默认 浏览器默认 无控制
block 约3秒 无限 Logo、品牌文字
swap 极短(约100ms) 无限 标题、强调文字
fallback 极短(约100ms) 约3秒 正文内容
optional 极短(约100ms) 低优先级装饰文字

Chrome开发者文档明确指出:optional是唯一能保证零布局位移的值。原因在于它的交换期为零——一旦过了极短的阻塞期,后备字体就"定型",不再被替换。

optional有一个微妙的权衡:浏览器可以根据网络状况决定是否下载字体。在慢速连接下,用户可能永远看不到自定义字体。这是性能与美学之间的理性妥协。

布局位移:被低估的代价

2020年,Google将Core Web Vitals纳入搜索排名因素,其中CLS(Cumulative Layout Shift)指标专门衡量视觉稳定性。字体加载导致的布局位移,突然从"美观问题"变成了"SEO问题"。

CLS的计算公式是两个分数的乘积:

布局位移分数 = 影响分数 × 距离分数
  • 影响分数:不稳定元素在视口中占据的区域比例
  • 距离分数:元素移动距离相对于视口最大尺寸的比例

举个例子:一个段落从视口上部移动到中部,影响了视口50%的区域(影响分数0.5),移动了视口高度的25%(距离分数0.25)。这个位移的分数就是0.5 × 0.25 = 0.125。

Google定义的阈值:

  • 良好:CLS ≤ 0.1
  • 需改进:0.1 < CLS ≤ 0.25
  • :CLS > 0.25

字体导致的布局位移尤其棘手,因为后备字体和自定义字体的度量(metrics)不同——字符宽度、行高、基线位置都可能变化。即使使用font-display: swap,替换瞬间的布局跳动也会被计入CLS。

字体度量覆盖:让后备字体"假装"成目标字体

2020年,CSS新增了一组@font-face描述符,允许开发者覆盖后备字体的度量值:

@font-face {
  font-family: 'My Fallback';
  src: local(Arial);
  ascent-override: 90%;
  descent-override: 22%;
  line-gap-override: 0%;
  size-adjust: 98%;
}

这些属性的含义:

  • ascent-override:基线以上的高度比例
  • descent-override:基线以下的高度比例
  • line-gap-override:行间距比例
  • size-adjust:水平缩放比例

Simon Hearne在他的博客中详细解释了这个技术:通过测量目标字体的度量值,然后调整后备字体使其匹配,可以显著减少替换时的布局跳动。

获取字体度量值的方法:使用FontDrop!网站上传TTF文件,在Data标签页查看hhea表中的ascender、descender、lineGap值,以及head表中的unitsPerEm值。

计算示例:如果字体的unitsPerEm为1000,ascender为1027,则ascent-override应为102.7%。

这个方案不是完美的——字间距和字符宽度仍然无法精确控制。但在大多数场景下,它能将CLS分数降低50%以上。

WOFF2:为什么压缩率不是唯一考量

2016年,W3C发布WOFF2规范。相比WOFF使用zlib压缩,WOFF2采用Brotli算法,压缩率提升约30%。

但WOFF2的优化不止于此。规范定义了一套预处理流程:

  1. 表重组:将字体表的存储顺序优化为解码友好的顺序
  2. glyf/loca表转换:对TrueType轮廓数据进行特殊处理,使其更易压缩
  3. 字符串去重:name表中的字符串使用共享存储

W3C的WOFF2评估报告显示,对于大型字体文件,这些预处理步骤的贡献可能超过Brotli压缩本身。

HTTP Archive 2022年的数据显示,WOFF2已占据字体请求的78%。但一个值得注意的现象是:自托管字体的WOFF2使用率仅为46%,远低于字体服务的78%。这意味着大量开发者仍在使用次优的压缩格式。

文件大小对比

字体类型 中位数大小(服务托管) 中位数大小(自托管)
全部字体 20 KB 37 KB
90分位数 75 KB 96 KB

差异的主要来源并非压缩格式,而是子集化(subsetting)。Google Fonts等服务会根据页面内容自动生成最小化子集,而自托管字体通常包含完整的字符集。

可变字体:一个文件,无限变化

2016年,OpenType 1.8引入了可变字体(Variable Fonts)技术。核心概念是:一个字体文件可以通过参数插值生成多个"实例"。

John Hudson在Medium上的技术文章解释了其工作原理:

一个OpenType可变字体包含一组默认轮廓,以及多个"轴"上的增量(deltas)。任何实例都是通过将增量应用到默认轮廓上插值计算得出。

技术上,可变字体的fvar表定义了轴的范围和命名实例。gvar表存储每个轴方向的轮廓增量。GDEF表中的Item Variation Store则处理GPOS/GSUB表的变量数据。

性能权衡

  • 优势:一个文件替代多个文件,减少HTTP请求数
  • 劣势:单文件体积更大,首次加载成本高

HTTP Archive 2025年的数据显示,约40%的网站已在使用可变字体。但使用场景的判断很重要:

场景 可变字体 静态字体
需要3个以上字重 ✅ 推荐 文件总体积更大
只需常规和粗体 ❌ 不推荐 总体积更小
响应式设计 ✅ 推荐 需要多文件
中文/日文 ⚠️ 谨慎 字符集太大

一个关键数据:可变字体替代6-12个静态字体文件,可节省50-75%的总体积。但如果只需要2个字重,静态字体的总体积反而更小。

资源提示:抢在浏览器前面

浏览器的字体加载时机存在一个固有问题:它必须等待CSSOM构建完成,才能知道哪些字体实际被使用。这意味着字体的下载请求总是在HTML和CSS解析之后。

渲染树构建流程

图片来源: web.dev

资源提示(Resource Hints)提供了一种绕过这个限制的方法:

<!-- 预连接:提前完成DNS、TCP、TLS -->
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>

<!-- 预加载:直接下载字体文件 -->
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>

HTTP Archive数据显示,preconnect的使用率从2020年的8%增长到2022年的15%,preload从17%增长到20%。

但预加载是一把双刃剑:它会抢占其他资源的带宽。Chrome的推荐是最多预加载2-3个关键字体文件,且应放在关键CSS之后。

CSS Font Loading API:JavaScript层面的控制

对于需要精细控制的场景,CSS Font Loading API提供了完整的JavaScript接口:

// 检测字体是否已加载
document.fonts.check('16px MyFont');

// 等待特定字体加载完成
document.fonts.load('16px MyFont').then(() => {
  document.body.classList.add('font-loaded');
});

// 监听字体加载状态变化
document.fonts.ready.then(() => {
  console.log('所有字体加载完成');
});

FontFaceSet接口还提供了状态追踪:

  • loading:有字体正在加载
  • loaded:所有字体加载完成
  • failed:有字体加载失败

这个API最常见的用途是实现"双阶段加载":首屏使用系统字体保证速度,加载完成后再切换到自定义字体——同时避免CLS惩罚。

选择策略:没有万能方案

综合所有技术,字体加载策略的选择框架:

页面类型 font-display 预加载 字体度量覆盖 可变字体
营销落地页 block 按需
内容网站 swap + fallback 标题字体
电商 optional 关键字体
SPA应用 swap 按需

核心原则:

  1. 首屏可见文字必须快速显示:使用swapfallback
  2. 避免布局位移:配置字体度量覆盖
  3. 减少文件体积:子集化 + WOFF2
  4. 控制请求数:合并字体或使用可变字体

数据背后的真相

HTTP Archive 2024年的统计揭示了一些值得反思的现象:

  • 84%的网站使用Web字体,比2020年的82%略有增长
  • Google Fonts占据65%的市场份额,但首次出现轻微下滑
  • 自托管字体的中位数体积(37 KB)几乎是服务托管字体(20 KB)的两倍
  • font-display: swap的使用率达到30%,但optional的使用率接近0

最后一项数据尤为值得关注:optional是唯一能保证零布局位移的值,但几乎无人使用。这反映出开发者对"字体可能永远不显示"的恐惧远超对布局位移的担忧。

Google Fonts在2019年将swap设为默认值,这可能是swap使用率飙升的主要原因——但它带来的布局位移问题,至今仍在影响大量网站的CLS分数。


参考资料

  • Zach Leatherman, “A Historical Look at FOUT and FOIT”, 2017
  • Chrome Developers, “Controlling Font Performance with font-display”, 2016
  • W3C, “WOFF File Format 2.0”, W3C Recommendation, 2024
  • W3C, “CSS Fonts Module Level 4”, Working Draft, 2024
  • MDN Web Docs, “CSS Font Loading API”
  • HTTP Archive, “Web Almanac 2022: Fonts”
  • HTTP Archive, “Web Almanac 2024: Performance”
  • Simon Hearne, “How to avoid layout shifts caused by web fonts”, 2021
  • John Hudson, “Introducing OpenType Variable Fonts”, Medium, 2016
  • Google Web.dev, “Cumulative Layout Shift (CLS)”
  • W3C, “OpenType Font Variations Overview”
  • IETF RFC 7932, “Brotli Compressed Data Format”
  • Tab Atkins, “CSS Font Display Controls Module Level 1”, 2018