← 返回博客

中文衬线字体从 1.7MB 压到 258KB

子集化不难,难的是知道该收哪些字。中间还猜错了一次,实测数据把我打回来了。

2026-09-02 约 2 分钟读完

本文目录 跳到想读的章节
  1. 只收会用到的字
  2. 变量字体比两个静态字重更省
  3. 我猜错的一次
  4. 差点漏掉一个字
  5. 结果
  6. 三条

站上的标题用衬线体。首次访问的总下载量是 1808KB,其中字体一个人占了 1692KB。

英文站不会有这个问题 —— 一套 Latin 字体几十 KB。中文的现实是:思源宋体一个字重就是 1.7MB 起步,而标题上真正用到的字,可能不到八百个。

只收会用到的字

思路很直白:扫一遍源码,把所有会被衬线渲染的字收集起来,只对这些字做子集。

关键在「会被衬线渲染」这个限定。全站所有中文有三千多字,但衬线只用在标题、引用、几个特定位置上。收多了等于白压。

所以收集器是按渲染位置来的,不是按文件:

// md 里:一到三级标题、引用块、frontmatter 的 title
/^#{1,3} /, /^> /, /^title: /
// astro 里:h1-h3、blockquote、title/lead/eyebrow 这几个 prop

跑下来 728 个字。按每个字四百多字节算,两百多 KB —— 数量级对了。

变量字体比两个静态字重更省

标题要用到常规和加粗两个字重。直觉上是压两个静态子集,各取所需。

实测:

方案体积
两个静态子集(400 + 700)333KB
一个变量字体(400–700 轴)150KB
const buf = await subsetFont(await fs.readFile(base), text, {
  targetFormat: 'woff2',
  variationAxes: { wght: { min: 400, max: 700 } },
});

变量字体只存一套字形轮廓加一组插值数据,而两个静态字重是把同一批字的轮廓存了两遍。字数越多,差距越大。

顺带把两个 @font-face 合成一个:

@font-face {
  font-family: PaperSerif;
  src: url(/fonts/paper-serif.woff2) format('woff2');
  font-weight: 400 700;   /* 一个区间,不是一个值 */
  font-display: swap;
}

我猜错的一次

压完之后我提了个假设:源字体是 TTF(二次曲线),换成 CFF/OTF(三次曲线)轮廓点更少,woff2 应该能再小一截。听起来很有道理。

量了一下:

源格式每字节数
OTF440 B
TTF419 B

差 5%。而且方向和我预期的相反。在中日韩字形这个量级上,woff2 的熵编码已经把曲线表示的差异吃掉了大半,省下来的那点还不够折腾一趟的。

这个假设写在提交信息里会很好看,可惜它是错的。量完就删掉了。

差点漏掉一个字

子集化最危险的失败模式是漏字:页面照样 200,控制台一声不吭,只有那一个字变成了系统字体,或者干脆是豆腐块。谁也不会天天盯着标题看。

所以加了一条断言,量的是真实覆盖:

ok('衬线子集没漏字', missing.length === 0, `字表 ${chars.length} 字`);

它当场就抓到一个:「月」

页面上到处是「2026 年 8 月」,但源码里搜不到这个字 —— 它是 cstYearMonth() 拼出来的:

export function cstYearMonth(d: Date) { return `${y} 年 ${m} 月`; }

收集器只扫了 md 和 astro,没扫 ts。补上 .ts 里的中文字符串字面量,还有那一串量词单位:

'年月日时分秒条本部张篇个第共约'

代码生成的文字不在任何模板里。 这类字最容易漏,也最难靠肉眼发现。

结果

之前之后
字体1692KB150KB
首次访问总量1808KB375KB

自托管还有个隐形的坑:--font-display 这个变量在全局样式里被定义了两次,后面那次没带 PaperSerif,结果自托管的字体从来没被请求过。看页面完全正常,因为回退字体也是衬线。

发现它靠的还是量:查计算后的 --font-display 里有没有 PaperSerif,以及有没有真的发出 woff2 请求。修法是把两处合并成一处,而不是调换顺序 —— 调换顺序下次再有人加一行照样坏。

三条

  1. 中文字体必须子集化,而且要按「哪里会用到这个字体」收集,不是全站扫一遍。
  2. 多字重优先变量字体,中文场景下差距是成倍的。
  3. 压完要验覆盖,验的是渲染结果不是文件大小。 漏字不会报错,只会悄悄难看。

读到这里,谢谢你的时间。

如果还想继续读下去,
新的文章和闪念会送进 RSS。

订阅这本档案 ↗