中文字体动辄几 MB,而一个页面往往只用到几十个字。于是有了好几种“别把整个字体都下载下来”的做法,它们经常被混着叫“按需加载”,但干的事完全不一样:
- 子集化(subsetting):先把字体裁到只剩要用的字,让文件变小;
- 切片 +
unicode-range:把字体切成几十上百块,让浏览器只下用到的那几块; - IFT(Incremental Font Transfer):一个文件,浏览器先拿一小块,缺什么再请求什么 —— 还在标准化。
下面逐个说,最后讲它们会弄坏什么。
两条路:少字形,还是少文件
| 做法 | 改的是 | 谁来做 | 内容能不能变 |
|---|---|---|---|
| 子集化 | 文件里的字形集合 | 构建期工具,或服务端接口 | 静态子集不能变;动态子集每次现生成 |
切片 + unicode-range |
声明一段码位对应一个文件 | 只写 CSS,切分是预先做好的 | 能变 —— 新字只是触发另一块下载 |
| IFT | 传输方式(范围请求 + 补丁) | 浏览器 + 服务器,都支持才生效 | 能变,且不用预先切分 |
一句话:子集化是“减少内容”,切片是“减少下载”,IFT 是“减少首次下载”。
子集化:把字体裁到只剩用到的字
通用工具是 fonttools 的 pyftsubset:
pyftsubset NotoSansSC-Regular.otf \
--text="字体检查器" \
--flavor=woff2 \
--output-file=noto-subset.woff2
--flavor=woff2 需要装 brotli。除此之外常见的还有按页面自动扫字的工具(扫 HTML 里的文本、生成子集),以及把这件事包成 GUI 的工具;本质都是同一件事:给定字符集,输出一个小字体。
它的好处是彻底 —— 文件可以直接从几 MB 掉到几十 KB。代价是:
- 字符集必须在生成时就确定。用户输入、后端返回的文本、以后改的文案,全都不在里面;
- 少了字就回落到下一个字体,混排时字重和骨架会突然不一样(见整形与回退);
- 子集字体只对这份页面有效,别指望装进系统:里面没有的字是真没有。
动态子集:把子集化放到服务端
接口收一段文本、当场生成子集返回,常见形式是:
/fonts/xxx.woff2?text=字体检查器
好处是内容可以变。代价在工程这侧:
- 每个不同的文本都是一个新资源。缓存键、CDN 都还算得过来(同一段文本永远同一个文件),但命中率取决于你的文本重复度;
- 文本进了 URL:它会出现在访问日志、代理日志、Referer 里。别把不该记的东西拼进去;
- 它要求有人替你托管字体、并且有权限生成子集——所以这条路基本是商业字库服务在做,自建则要自己管好这一步。
切片 + unicode-range:只下用到的那几块
这是纯 CSS 的做法,不需要服务端动手:
@font-face {
font-family: 'Example SC';
src: url('/f/ex-common.woff2') format('woff2');
unicode-range: U+4E00-9FFF; /* 这一块覆盖的码位 */
}
@font-face {
font-family: 'Example SC';
src: url('/f/ex-ext.woff2') format('woff2');
unicode-range: U+3400-4DBF;
}
同一个族名可以有任意多条 @font-face,每条带一段 unicode-range。浏览器排版时会按文本里实际出现的码位决定:这些文件里哪些需要下载。页面没出现对应范围的字符,那个文件一个字节都不发。
⚠️ 关键区分:unicode-range 决定的只是「这个文件要不要下」,不是「只下其中这几个字」。一旦某一块被需要,它里面的字会整块下下来 —— 所以效果完全取决于你怎么切。
中文字体的通行切法是:把最常用的几千字单独成一块(或按频次分成几块),其余按码位区段切,一块一两百字。Google Fonts 上的中文字体就是这么发的 —— 你去看它的 CSS,一个字体有上百条 @font-face,每条一段 unicode-range。
这套做法的麻烦在于配置与维护:分片方案要自己生成、文件数会暴涨、改一次分片就得全量重发。好处是它扎实 —— 浏览器原生支持,不依赖任何服务端能力。
IFT:一个文件,渐进获取
切片是“事先猜你会用到哪些字”。IFT(Incremental Font Transfer)想做的是“不用猜”:浏览器用范围请求向服务端要字体文件的一部分,先拿到能显示首屏的那一小块;遇到缺的字形,再按需请求补丁,把字形追加进来。
W3C 正在标准化这件事(有独立规范的草案),Chrome 侧已有实验性实现,默认没开。它要成立的前提是服务端支持范围请求与补丁,所以短期内的现实地位是“知道有这么个方向”,不是“明天就能用”。
子集化会弄坏什么
| 弄坏的东西 | 后果 |
|---|---|
GSUB / GPOS / GDEF 被一起裁掉 |
连写、上下文替换、标记定位全没了:阿拉伯文、印度各文字会直接排错,不只是变丑 |
变量字体的 fvar / gvar 被裁掉 |
剩下的字能显示,但字重轴没了,变成“看起来是变量字体、实际只能用一档” |
只保留部分字形,却留了完整 cmap |
文本里有码位、文件里没字形,浏览器会报缺字或悄悄画个空框 |
| 字符集没覆盖到 | 回落字体混排,字重与骨架突变;中文尤其明显 |
| 切得太细 | 每个分片都有自己的表结构开销,10 字一片反而总字节更多;HTTP/2 多路复用能缓解延迟,但不减少总字节 |
name 表被裁 |
还想当系统字体安装时,系统里认不出来源、样式名会退化 |
还有一条不是技术问题的:能不能子集化,字体自己就写着。OS/2 表的 fsType 里有一位就是「禁止子集化」(0x0100),另外几位分别是受限嵌入、仅预览与打印、可编辑嵌入、仅位图嵌入。做 Web 子集、往 PDF 里嵌字体之前,先看这一位 —— 见 OS/2 表。
什么时候不值得做
先问三个问题,任何一个答不上来就别做:
- 文本真的已知吗? 会变的文本只能走切片或动态子集,静态子集是在给自己埋缺字;
- 文件真的太大吗? 拉丁字体做完 WOFF2 往往只有几十 KB,子集化省下的那点量不值得多一套构建步骤;
- 许可允许吗?
fsType说了不许就是不许。
本站的情况
本站不给页面下发任何字体文件:界面用的是系统字体栈,字体库里的展示图是入库时预渲染好的图片。所以上面这些手法,在本站的页面上一样都没用到。
检查器里的「临时加载」也不算 —— 那是把你自己机器上那个字体文件的字节,在当前标签页里用 FontFace 注册一下,刷新即失效,字节不会上传。
本站不提供子集化或切片服务,也没有现成的子集文件地址可以给你拄。上面讲的是通用做法,真要做,得自己在构建流程里跑工具,或者自己搭一个动态子集的接口。
延伸阅读
- fonttools 文档里的
pyftsubset(参数齐全,包括保留哪些表) - MDN:
@font-face的unicode-range描述符 - CSS Fonts 规范:
unicode-range与字体下载时机的规定 - W3C 的 Incremental Font Transfer 草案