Web 字体优化:从加载到渲染的全链路调优
Web 字体这东西,看着简单,用起来坑不少。我早期做的一个内容型网站,就因为引了一款花哨的英文字体,首屏加载硬生生多了 3 秒。用户反馈说「页面白了好久,然后字突然闪了一下」——嗯,那就是典型的 FOIT 和 FOUT 问题。
这一章,咱们把 @font-face 规则、字体子集化、WOFF2 压缩,还有加载策略串起来讲。说白了,就是让字体文件变小、加载变快、渲染不闪。
@font-face 规则:别只写个 src 就完事
很多人写 @font-face 就一行 src,然后扔个 woff2 上去。其实这里面有不少细节。
先看一个我推荐的基础写法:
@font-face {
font-family: 'MyFont';
src: url('myfont.woff2') format('woff2'),
url('myfont.woff') format('woff');
font-display: swap;
font-weight: 400;
font-style: normal;
unicode-range: U+4E00-9FFF; /* 仅中文字符 */
}
这里有几个关键点:
- src 顺序:WOFF2 放前面,WOFF 做降级。浏览器会按顺序下载第一个能识别的格式。
- font-display:控制字体加载期间的渲染行为。我后面会细讲。
- unicode-range:限定字符集范围。这个对中文字体特别有用。
字体子集化:把 20MB 的中文字体砍到 200KB
中文字体为什么大?因为常用汉字就有 6000 多个,加上标点、符号,一个完整的 GB2312 字库至少 7000+ 字符。英文字体才 200 多个字符,你说差距大不大?
字体子集化,就是只保留你页面实际用到的字符。我在一个电商项目中,把一款 12MB 的思源黑体,子集化后只剩 180KB——因为那个页面只用了 300 多个汉字。
常用的子集化工具有:
| 工具 | 适用场景 | 特点 |
|---|---|---|
| glyphhanger | 命令行,适合 CI/CD | 自动扫描 HTML/JS 提取字符 |
| fonttools (pyftsubset) | Python 生态 | 灵活,支持高级选项 |
| FontSquirrel Webfont Generator | 在线工具,快速验证 | 可视化操作,适合新手 |
| subfont | Node.js 生态 | 自动集成到构建流程 |
我用 glyphhanger 比较多,命令很简单:
# 先扫描页面用到的字符
glyphhanger https://example.com --spider --css
# 然后子集化
pyftsubset source.woff2 --unicodes-file=glyphhanger.txt --output-file=subset.woff2
WOFF2 压缩:比 WOFF 再小 30%
WOFF2 是当前 Web 字体格式的终点站。它用 Brotli 算法做预处理,压缩率比 WOFF 高 30% 左右。而且所有现代浏览器都支持了,没必要再纠结 EOT 或 SVG 字体。
转换工具推荐:
- Google woff2:官方工具,命令行操作
- FontForge:图形界面,支持批量
- 在线转换:transfonter.org,方便快捷
命令行转换示例:
# 安装 woff2 工具
brew install woff2
# TTF 转 WOFF2
woff2_compress myfont.ttf
# WOFF2 转 TTF(调试用)
woff2_decompress myfont.woff2
你想想看,一个 500KB 的 TTF 文件,转成 WOFF2 可能只剩 150KB。再配合子集化,最终可能只有 30-50KB。这个优化空间,值得花时间做。
字体加载策略:FOUT vs FOIT
这两个概念,我当年踩过坑才真正理解。
- FOIT (Flash of Invisible Text):字体加载期间文字不可见。用户看到的是空白。
- FOUT (Flash of Unstyled Text):先用后备字体显示,等 Web 字体加载完再切换。用户看到的是字体「闪变」。
哪个更好?说实话,没有绝对答案。但我个人更倾向 FOUT。为什么?因为用户至少能先看到内容,而不是盯着白屏等字体加载。
控制策略靠 font-display 属性:
| 值 | 行为 | 适用场景 |
|---|---|---|
| auto | 浏览器默认(通常是 FOIT) | 不推荐,行为不可控 |
| block | FOIT,最多等 3 秒 | 品牌字体,必须显示 |
| swap | FOUT,立即用后备字体 | 内容优先,推荐 |
| fallback | FOIT 短时间(约 100ms),然后 FOUT | 折中方案 |
| optional | 如果字体没缓存,就用后备字体 | 非关键字体 |
我一般这样用:
/* 正文用 swap,保证内容可读 */
body {
font-family: 'MyFont', system-ui, sans-serif;
}
/* 标题用 fallback,稍微等一下品牌字体 */
h1, h2, h3 {
font-family: 'MyFontDisplay', serif;
}
完整优化流程:一张图说清楚
下面这张图,是我在实际项目中总结的字体优化流程。从原始字体文件到最终上线,每一步都有明确目标。
避坑指南:我踩过的几个坑
讲几个真实案例,你遇到了能少走弯路。
我曾经用某个在线工具子集化一款商业字体,结果生成的文件在 iOS 上完全显示不出来。排查了半天,发现是工具没正确处理字体中的 hinting 信息。后来改用 fonttools 的 pyftsubset,问题解决。
后备字体和 Web 字体宽度不一致,加载完成后文字重排,页面内容「跳」了一下。解决方案:用
size-adjust 属性调整后备字体的度量值,或者选择宽度相近的后备字体。
老版本 Safari 对 unicode-range 支持有问题。我的做法是:先不加 unicode-range 做全量加载,等页面稳定后再逐步加上。或者用
@supports 做特性检测。
总结一下
Web 字体优化,核心就三件事:
- 让文件变小:子集化 + WOFF2,这是最立竿见影的优化
- 让加载变快:合理的 @font-face 声明 + 预加载提示
- 让渲染不闪:font-display 策略 + 后备字体匹配
做完这三步,你的字体加载体验基本就合格了。剩下的就是上线后监控,看看有没有异常情况。