文档

按需加载与子集化

让字体文件变小、下载变少的三种办法:子集化、unicode-range 切片、IFT 增量传输,以及它们各自会弄坏什么。

最后更新 2026-11-04

  • 字体子集化
  • subsetting
  • pyftsubset
  • unicode-range
  • 字体切片
  • 按需加载
  • IFT
  • 增量字体传输

中文字体动辄几 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 表。

什么时候不值得做

先问三个问题,任何一个答不上来就别做:

  1. 文本真的已知吗? 会变的文本只能走切片或动态子集,静态子集是在给自己埋缺字;
  2. 文件真的太大吗? 拉丁字体做完 WOFF2 往往只有几十 KB,子集化省下的那点量不值得多一套构建步骤;
  3. 许可允许吗? fsType 说了不许就是不许。

本站的情况

本站不给页面下发任何字体文件:界面用的是系统字体栈,字体库里的展示图是入库时预渲染好的图片。所以上面这些手法,在本站的页面上一样都没用到。

检查器里的「临时加载」也不算 —— 那是把你自己机器上那个字体文件的字节,在当前标签页里用 FontFace 注册一下,刷新即失效,字节不会上传。

本站不提供子集化或切片服务,也没有现成的子集文件地址可以给你拄。上面讲的是通用做法,真要做,得自己在构建流程里跑工具,或者自己搭一个动态子集的接口。

延伸阅读

  • fonttools 文档里的 pyftsubset(参数齐全,包括保留哪些表)
  • MDN:@font-face 的 unicode-range 描述符
  • CSS Fonts 规范:unicode-range 与字体下载时机的规定
  • W3C 的 Incremental Font Transfer 草案