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属性,本质上是在控制字体下载生命周期的三个阶段:
- 阻塞期(Block Period):如果字体未加载,使用不可见的后备字体
- 交换期(Swap Period):如果字体未加载,使用可见的后备字体,字体加载后立即替换
- 失败期(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的优化不止于此。规范定义了一套预处理流程:
- 表重组:将字体表的存储顺序优化为解码友好的顺序
- glyf/loca表转换:对TrueType轮廓数据进行特殊处理,使其更易压缩
- 字符串去重: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 |
❌ | ✅ | 按需 |
核心原则:
- 首屏可见文字必须快速显示:使用
swap或fallback - 避免布局位移:配置字体度量覆盖
- 减少文件体积:子集化 + WOFF2
- 控制请求数:合并字体或使用可变字体
数据背后的真相
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