给站点字体做子集化:从十几 MB 到几百 KB

2026/01/17

这阵子给站点换字体时踩了个经典的坑:中文字体太「重」了。一套思源黑体 Regular 的 OTF 动辄十几 MB,直接丢到线上,首屏字体请求能把加载时间拖垮一大截。全量字库里躺着几万个字形,可一个博客站点真正用到的基本汉字,撑死也就几千个。为了那 90% 永远用不到的字付全款,不划算。

于是有了这个小工具:按站点实际用字对字体做子集化,把几 MB 的全量字体瘦身成几百 KB 的版本,只保留需要的字符,同时保留完整的排版特性。这篇文章记录一下思路和用法。

一、字体为什么这么大,子集化到底做了什么

字体文件的体积和它包含的字形数量直接相关。以中文字体为例:

  1. 《通用规范汉字表》一二级合计 6500 个汉字,覆盖日常阅读的绝大多数场景;
  2. 完整的商业/开源中文字体往往收录数万个字形,还带着各种语言的字符集;
  3. OTF/TTF 格式还要存轮廓、排版特性、字形名称等元数据,体积进一步膨胀。

子集化(subset)做的就是「按需裁剪」:给定一份字符表,只保留这些字符对应的字形,其余全部丢弃。同样的字体,从全量十几 MB 缩到只含 6500 常用字,通常能压到 1–2 MB;如果再按站点实际用字裁剪,往往只剩几百 KB,配合 woff2 压缩后更小。

二、字表从哪来:通用 6500 字 + 站点实际用字

子集化第一步是确定「要留哪些字」。这个工具支持三档:

  1. 通用档(6500):以国务院公布的《通用规范汉字表》一二级 6500 字为底,再加上拉丁字母、数字和常用中英文标点。适合内容不固定、或不想维护站点字表的场景,一把梭。
  2. 站点档(site):自行扫描站点的全站文字,导出站点实际出现过的所有字符,得到一个站点专属字表。不限于 WordPress,Next.js 或任何其他站点都可以,只要能扫出全站用字表就能用。体积最极致,但站点新增内容后字表要跟着更新。
  3. 合并档(merged):通用 6500 ∪ 站点实际用字,去重合并。既覆盖日常内容,又不会漏掉站里出现过的生僻字,是我最常用的组合。

通用字表用 build-charset.sh 从一二级字表重建,合并档用 merge-charset.sh 生成,都是纯本地脚本。

三、子集化本身:pyftsubset 封装

核心工具是 fonttools 自带的 pyftsubset,我用一个 subset.sh 把它按站点包起来。用法很简单:

# 只装依赖
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt   # fonttools + brotli

# 放好源字体后,按站点执行
SITE=site1 bash subset.sh                    # 通用 6500 字表
SITE=site1 CHARSET=site bash subset.sh       # 仅站点用字
SITE=site1 bash merge-charset.sh             # 先合并
SITE=site1 CHARSET=merged bash subset.sh     # 再用合并字表

脚本会自动处理源目录里的 ttf/otf/ttc/woff/woff2 全部文件,逐个子集化。几个实用细节:

  1. 输出格式默认随源字体扩展名:otf 出 .subset.otf,woff2 出 .subset.woff2;也可以用 OUTPUT_FORMAT=woff2 强制把 otf 源直接转成 woff2 给线上用。
  2. ttc 合集:可指定 FONT_NUMBER 抽取其中某个字重,子集结果是单个字体文件。
  3. 排版特性完整保留--layout-features='*'--glyph-names--name-IDs='*' 等参数确保子集后字体在浏览器里的渲染行为和原来一致,不会出现特性丢失。
  4. 生成 woff2 需要 brotli,只做 ttf/otf 子集可以不装。

四、多站点怎么隔离

这个工具最初就是按「一个仓库管多个站点」设计的,所有输入输出都按站点目录隔离:

source/<SITE>/       # 源字体,不入库
output/<SITE>/       # 子集产物,不入库
charset-site/<SITE>/ # 站点用字表,不入库
charset/             # 通用字表,入库

每个站点一套字表、一套产物,互不干扰。SITE 是必填参数,脚本也做了字符白名单校验,防止误操作写进根目录。

五、替换到站点时要注意的

子集做完,替换上线还有几个点要顺手处理:

  1. output/<SITE>/ 里的产物替换主题的字体文件(比如 assets/fonts/)或 OSS 上的字体。
  2. 同步改 @font-face 里的 format(),和实际文件扩展名保持一致;如果换成了 woff2,记得把 preload 也更新。
  3. 表外汉字没有字形,会回退到 font-family 后面的系统字体。站点档字表越激进,这个风险越高,所以更推荐「通用 ∪ 站点用字」的合并档。
  4. 字体授权别踩坑:思源黑体 / Noto Sans SC 这类开源字体随便用,PingFang 这类要先确认授权。

六、效果

以我的博客为例,正文用思源黑体,全量 OTF 约 12 MB,子集化到 6500 通用字后约 1.5 MB,再按站点实际用字合并裁剪并转成 woff2,稳定在几百 KB 量级。字体加载对首屏的拖累基本可以忽略,页面整体加载速度改善非常明显。

脚本只做本地文件处理,不会自动改 WordPress 或上传 OSS,替换动作始终由自己掌控。代码已开源在 GitHub(biancaplus/font-subset),欢迎拿去用,遇到问题直接提 Issue。