网页字体的性能困境

网页字体(Web Font)是现代网站设计中不可或缺的元素,它能显著提升品牌的视觉识别度和页面的设计质感。然而,字体文件往往是页面中体积较大的资源之一——一个完整的字体族(包含常规、粗体、斜体等多个字重)总大小可能达到 1-2MB,中文字体文件更是动辄 5-10MB。

未优化的字体加载会导致 FOIT(Flash of Invisible Text,文字不可见闪烁)或 FOUT(Flash of Unstyled Text,无样式文字闪烁)现象,严重影响用户体验和 Core Web Vitals 中的 LCP 指标。根据 Google 的研究,字体加载优化可以使 LCP 平均提升 15-20%。本文将详细介绍网页字体的优化加载策略。

举个直观的例子:一个同时引入 400、700 两个字重、外加斜体的英文网站,字体文件合计通常超过 1MB;而中文字体因为包含数万个字形,一个思源黑体 Regular 的单字重 woff2 就有 4-5MB。把这些文件不加处理地挂在页面上,首屏渲染就会被严重拖慢。接下来的策略都围绕同一个目标:让“该用的字尽快用上、不该用的字一个都不下载”。

一、字体加载的核心概念

1.1 字体显示策略

CSS 提供了 font-display 属性来控制字体加载期间的文字显示行为:

属性值 行为 适用场景
auto 浏览器默认策略(通常是 FOIT) 通用场景
block 短暂阻塞(~3s 不可见,之后降级) 品牌字体关键使用
swap 立即显示降级字体,加载后替换 大多数场景(推荐)
fallback 短暂阻塞,若超时则长期使用降级字体 平衡美观与性能
optional 极短阻塞,若超时则不再加载 追求极致性能
@font-face {
  font-family: 'MyFont';
  src: url('/fonts/myfont.woff2') format('woff2');
  font-display: swap; /* 推荐大多数场景使用 */
}

1.2 字体格式对比

格式 压缩率 浏览器支持 说明
WOFF2 最佳 现代浏览器均支持 首选格式
WOFF 广泛支持 兼容旧浏览器
TTF/OTF 无压缩 全支持 不推荐用于 Web
EOT 一般 仅 IE 已淘汰

实际工程中,同一款字体用不同格式保存,体积差异非常明显。以思源黑体(Source Han Sans)简体中文 Regular 字重为例:TTF 原始文件约 16MB,转成 WOFF 约 10MB,再转成 WOFF2 约 4-5MB。如果只子集化到常用汉字,woff2 可以压到几百 KB。这也是为什么“WOFF2 + 子集化”几乎成为中文字体优化的标准组合。

二、优化策略

2.1 字体子集化

对于中文字体,子集化是最有效的优化手段。中文字体包通常包含 20000+ 字符,但一个典型网站实际使用的字符不到 2000 个。通过子集化可以移除未使用的字符,将字体文件缩小 90% 以上:

# 使用 glyphhanger 提取页面使用的字符
glyphhanger --whitelist="常用汉字和标点" --subset=*.ttf --format=woff2

如果不依赖 glyphhanger,用开源工具 pyftsubset(来自 fonttools)也能完成同样的工作:

pyftsubset SourceHanSansSC-Regular.otf \
  --text-file=chars.txt \
  --output-file=subset.woff2 \
  --flavor=woff2 \
  --layout-features='*'

其中 chars.txt 是页面实际用到的字符列表,可以从构建产物中提取。对纯中文内容,通常保留 3000-5000 个字符就能覆盖绝大多数页面;加上 unicode-range 分段后,浏览器可以按需只下载含目标文字的片段。

2.2 预加载关键字体

<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>

2.3 字体加载策略组合

方案 复杂度 性能 可维护性 适用场景
font-display: swap 所有项目
字体子集化 极高 中文/日文等大字符集
预加载关键字体 首屏文字
内联关键字体 极高 首屏字体极少时
自托管字体 避免第三方依赖

2.4 自托管 vs 第三方字体服务

<!-- Google Fonts 加载(不推荐用于性能敏感项目)-->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet">

<!-- 自托管时只需使用 @font-face -->
<style>
  @font-face {
    font-family: 'Inter';
    src: url('/fonts/inter-regular.woff2') format('woff2');
    font-display: swap;
    unicode-range: U+0000-00FF; /* 限制 Unicode 范围 */
  }
</style>

如果因为团队协作等原因暂时保留第三方字体服务,至少用 preconnect 提前建立连接,让浏览器更早发出请求:

<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

自托管虽然增加了一点维护成本,但能完全控制缓存、font-display 和真正下发哪些字重,在高延迟网络下收益尤其明显。

三、代码示例

// 使用 Font Face Observer 检测字体加载完成
import FontFaceObserver from 'fontfaceobserver';

const font = new FontFaceObserver('MyFont');

font.load(null, 3000).then(() => {
  document.documentElement.classList.add('fonts-loaded');
}).catch(() => {
  document.documentElement.classList.add('fonts-failed');
});
/* 字体加载前使用系统字体 */
body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}

/* 字体加载后切换 */
.fonts-loaded body {
  font-family: 'MyFont', sans-serif;
}

四、注意事项

  1. FOUT 比 FOIT 更好:用户宁愿先看到系统字体的文字,也不愿看到空白
  2. 限制字重数量:每增加一个字重(bold、semibold 等)都意味额外的下载
  3. 使用 variable fonts:可变字体一个文件包含多个字重,大幅减少请求数
  4. 缓存策略:字体文件应设置长缓存时间(一年以上),因为字体很少更新

五、字体加载的常见问题

为什么字体加载了但页面还是用了系统字体? 最常见的原因是 font-display: swap 超时或字体文件未随页面成功下载——检查 Network 面板里字体请求是否返回 200,以及 @font-facesrc 路径是否与部署目录一致。

为什么中文字体页面首屏特别慢? 中文字体动辄数 MB,问题基本都出在“整包加载”。先做子集化,再配合 unicode-range 分段加载,首屏只下载包含标题、按钮等关键文字的片段,通常能把体积从 5MB 降到 500KB 以内。

如何验证字体是否真的加载完成了? 打开 Chrome DevTools → Network 过滤 Font,可以看到每个字体文件的下载大小、耗时与优先级;用 Performance 面板录制页面加载,能直观看到字体阻塞首屏渲染的时间段。

参考:MDN font-display https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display;Google web.dev 字体优化指南 https://web.dev/articles/font-best-practices;fonttools/pyftsubset https://fonttools.readthedocs.io/en/latest/subset/index.html

六、总结

网页字体优化需要在设计品质和加载性能之间找到平衡点。建议从最基础的措施开始——使用 WOFF2 格式、设置 font-display: swap、预加载关键字体。对于中文等大字符集字体,子集化几乎是必须的优化步骤。通过组合使用这些策略,你可以在不影响品牌视觉效果的前提下,将字体加载对页面性能的影响降到最低。