把 24 MB 的中文字体拆成按需加载
这个站的正文用霞鹜文楷。Regular 与 Medium 两档字重的完整 TTF 分别是 24.39 MB 和 24.20 MB,加起来 48.59 MB。
拉丁字体不存在这个问题——覆盖常用字符只要几十 KB。中文字库要覆盖上万个汉字,量级差三个数量级。整包用 @font-face 引上去,首屏就得等它下完。
unicode-range
@font-face 有个描述符叫 unicode-range,用来声明这个字体文件覆盖哪些 Unicode 码位。浏览器解析完页面、知道要渲染哪些字符之后,只下载覆盖了这些码位的文件。
于是可以把一个完整字体切成很多小片,每片各自声明覆盖范围。切分用 cn-font-split:
import { fontSplit } from 'cn-font-split';
import fs from 'node:fs';
await fontSplit({
input: new Uint8Array(fs.readFileSync('LXGWWenKai-Regular.ttf').buffer),
outDir: './public/fonts/lxgw-400',
css: {
fontFamily: 'LXGW WenKai',
fontWeight: '400',
fontDisplay: 'swap',
},
});
生成的样式表里,每条规则对应一个分片:
@font-face {
font-family: "LXGW WenKai";
src: url("/fonts/lxgw-400/7035b752.woff2") format("woff2");
font-weight: 400;
font-display: swap;
unicode-range: U+9F11-9F12, U+9F14-9F16, U+9F18;
}
切完是什么样
| 切分前 | 切分后 | |
|---|---|---|
| 文件数 | 2 | 644 |
| 总体积 | 48.59 MB | 25.41 MB |
| 最大单文件 | 24.39 MB | 105.4 KB |
| 平均单文件 | 24.30 MB | 40.4 KB |
总体积降了一半,是 TTF 转 woff2 的压缩带来的,和切分无关。切分带来的收益不在总量,在于总量不再需要一次下完。
你正在看的这一页,实测下载了 34 个中文分片,合计 1091 KB,平均每片 32 KB。约占完整字体的百分之二。
一个必须改掉的默认
生成的 src 默认长这样:
src: local("LXGW WenKai"), url("/fonts/lxgw-400/7035b752.woff2") format("woff2");
local() 排在前面,意思是:如果访客本机装了同名字体,直接用本地文件,不发网络请求。
这条默认有两个后果。一是加载结果随访客机器而变,同一个页面在两台电脑上走两条不同的路径。二是它会让你无法验证自己的配置——这一点在下面那个坑里会要命。
处理办法是把 local(...) 从生成的样式表里删掉,让字体只有一条来源。
那个坑
配好之后我想确认字体真的生效了,第一反应是在控制台跑:
document.fonts.check('16px "LXGW WenKai"'); // true
返回 true,看着没问题。
但这个判断是错的。document.fonts.check() 检查的是“这个字体当前可用吗”,它不区分字体是从网络加载的还是本机装着的。而我的机器上恰好装了这个字体——另一条排版工具链早就把它装进了 ~/.local/share/fonts。
也就是说:哪怕网页的 @font-face 完全失效,这个检查照样返回 true。 我本机看着一切正常,访客那边早已回退到宋体。
同理,“首屏字体字节数远小于完整字体”也证明不了什么——它可能只是加载了别的字体。
唯一与本机字体安装无关的证据是网络请求:
- 开发者工具的网络面板按 Font 过滤
- 确认存在
/fonts/lxgw-400/*.woff2的请求,且状态是 200 - 抽一个请求的 URL,确认它出现在生成的样式表里
第三步不能省。只看到“有 woff2 请求”不够,得确认请求的正是你生成的那批文件。
代价
样式表本身变大了。644 条 @font-face 规则,原始 440 KB,brotli 压缩后线上实测传输 108 KB。
这是一笔固定开销,每个访客首次访问都要付。缓解办法是把字体目录标成永久不可变缓存:
cache-control: public, max-age=31536000, immutable
分片文件名带内容哈希,内容变了文件名就变,所以可以放心设一年。样式表和分片都只拉一次,之后跨页面复用。
先看授权,再看体积
子集化只减小体积,不改变授权。商业字体切成分片,仍然是那份商业字体。
网页字体是分发行为,不是使用行为:每个访客的浏览器都会下载一份字体文件到本地。字体授权通常把桌面排版和 Webfont 分发分开授权,“我不商用”这个前提不自动推出“我可以把字体发给所有访客”。
这个站为此换过一次字体,经过写在开张那篇里。