字体性能优化:font-display、子集化与可变字体
字体性能优化真正的大头是「少下载」而不是「晚渲染」:先用子集化 + woff2 把字体文件砍掉 90% 以上体积,再去调 `font-display`、再决定要不要上可变字体。顺序颠倒——一上来就加 `font-display: swap`——最多让文字早出现半秒,几 MB 的字体照样要传完,等于没优化。
先算账:字体的成本在传输字节
结论:一个中文字体的体积损失,比任何加载策略都值钱,先盯体积再谈策略。
一套常见中文无衬线字体,TTF 动辄 5-10MB;转成 woff2(Web Open Font Format 2,用 Brotli 压缩的网页字体格式)后大约 2-4MB;而只保留实际用到的字符后,通常能压到 20-150KB。同样的请求数、同样的网络条件,这一步就是 20-50 倍的差距,`font-display` 再优化也换不来这个量级。
所以排查顺序是:这个字体非用不可吗?→ 能子集化吗?→ 能转 woff2 吗?→ 最后才问「加载策略怎么选」。
子集化:一次砍掉 90% 体积
结论:静态文案用 `pyftsubset` 按实际字符集裁剪,UGC 场景用 `unicode-range` 分片,两种做法别混用。
静态站点最省事的做法是用 fonttools 直接裁:
pyftsubset SourceHanSansSC-Regular.otf \
--text-file=used-chars.txt \
--flavor=woff2 \
--layout-features='*' \
--no-hinting \
--output-file=subset.woff2
`used-chars.txt` 就是页面里出现过的所有字符,可以写个脚本爬一遍模板自动生成。注意把中英文标点、全角符号、数字一起收进去,否则会出现「汉字是自定义字体、逗号是系统字体」的割裂感。
如果站点有用户生成内容(昵称、评论、帖子),静态子集必然缺字。这时改用 `unicode-range` 分片:把常用汉字按频率切成若干个文件,配合多个 `@font-face` 声明,浏览器只下载当前页面真正命中的分片。
@font-face {
font-family: 'HS';
src: url('/fonts/hs-common.woff2') format('woff2');
unicode-range: U+4E00-9FFF; /* 常见汉字区间,实际按频率再细分 */
font-display: swap;
}
中文分片的通用经验是:前 3000 常用字单独一个文件(这部分覆盖日常正文 99% 以上),剩余生僻字按 Unicode 区间切片,每片控制在 30KB 以内。
font-display:五个值对应三种场景
结论:正文用 `swap` 或 `optional`,品牌标题用 `block` 加 preload,`auto` 永远不要留。
`font-display` 是 `@font-face` 的描述符,控制字体没加载完时文字怎么显示。它本质是两个时间段——block 期(文字不可见)和 swap 期(用兜底字体先显示)——的组合:
| 取值 | block 期 | swap 期 | 效果 |
|---|---|---|---|
| `block` | 约 3s | 无限 | 先空白,最长 3s 不可见,之后换字体 |
| `swap` | 极短 | 无限 | 立刻用兜底字体显示,加载完替换(FOUT) |
| `fallback` | 约 100ms | 约 3s | 折中,短暂空白后兜底,3s 内到货才替换 |
| `optional` | 约 100ms | 0 | 100ms 内没到就本次不用,浏览器自行决定是否继续下载 |
怎么选:正文阅读区首选 `swap`(内容优先),对布局稳定性要求高的用 `optional`(配合 preload,第二次访问直接命中缓存);品牌标题、Logo 这类对字形敏感的用 `block` 加 preload,但一定要给 `font-display` 设个上限,别让文字长时间空白。
`auto` 等同于浏览器默认(多数是 `block`),行为不可控,显式写出来。
可变字体:一个文件替代多个字重
结论:需要 3 个以上字重时才上可变字体,1-2 个用静态子集更划算。
可变字体(Variable Font)把字重、字宽、倾斜等维度做成可调节的「轴」,一个文件顶替 Regular/Medium/Bold 等多份文件。声明方式:
@font-face {
font-family: 'InterVar';
src: url('/fonts/inter-var.woff2') format('woff2');
font-weight: 100 900; /* 声明支持的轴范围 */
font-display: swap;
}
但要清楚它的代价:一个可变字体文件通常比单个静态字重大 30%-60%,比两个静态字重小,比四个静态字重小得多。所以只有站点确实用到 3 个以上字重时,它才划算。
另一个坑是可变字体 + 子集化会互相拖累。如果最终只用某一两个字重,可以用 fonttools 把它「实例化」成静态文件,体积立刻回落:
fonttools varLib.instancer inter-var.ttf wght=400 -o inter-400.ttf
两个最容易踩的收尾坑
结论:字体要么加 `crossorigin` 的 preload,要么别 preload;同时用度量覆盖压住 CLS。
`swap` 带来的最大副作用是 FOUT 引起的布局偏移(CLS)。解决办法是给兜底字体做「度量覆盖」,让它的行高、字宽尽量贴近目标字体:
@font-face {
font-family: 'Fallback';
src: local('Arial');
size-adjust: 107%;
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
}
另外两个细节:跨域字体必须在服务器返回 `Access-Control-Allow-Origin`,否则会被 CORS 拦掉;preload 时 `<link rel="preload" as="font" type="font/woff2" crossorigin>` 的 `crossorigin` 一个字符都不能少,否则浏览器会下载两次。字体文件本身用 `Cache-Control: public, max-age=31536000, immutable` 长缓存,文件名带内容哈希即可放心永久缓存。
把整套流程串起来就是:统计字符 → `pyftsubset` 裁剪 + 转 woff2 → 按需分片或选可变字体 → 设 `font-display` 与度量覆盖 → preload 关键字体 + 长缓存。按这个顺序做,中文字体从 5MB 到 100KB 级的落地是完全可复现的。
转载请注明出处,版权归原作者所有。
星耀SVIP
管理员
黑卡会员





