把 24 MB 的中文字体拆成按需加载

2026年8月19日 · 字体 · 前端

这个站的正文用霞鹜文楷。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 我本机看着一切正常,访客那边早已回退到宋体。

同理,“首屏字体字节数远小于完整字体”也证明不了什么——它可能只是加载了别的字体。

唯一与本机字体安装无关的证据是网络请求:

  1. 开发者工具的网络面板按 Font 过滤
  2. 确认存在 /fonts/lxgw-400/*.woff2 的请求,且状态是 200
  3. 抽一个请求的 URL,确认它出现在生成的样式表里

第三步不能省。只看到“有 woff2 请求”不够,得确认请求的正是你生成的那批文件。

代价

样式表本身变大了。644 条 @font-face 规则,原始 440 KB,brotli 压缩后线上实测传输 108 KB。

这是一笔固定开销,每个访客首次访问都要付。缓解办法是把字体目录标成永久不可变缓存:

cache-control: public, max-age=31536000, immutable

分片文件名带内容哈希,内容变了文件名就变,所以可以放心设一年。样式表和分片都只拉一次,之后跨页面复用。

先看授权,再看体积

子集化只减小体积,不改变授权。商业字体切成分片,仍然是那份商业字体。

网页字体是分发行为,不是使用行为:每个访客的浏览器都会下载一份字体文件到本地。字体授权通常把桌面排版和 Webfont 分发分开授权,“我不商用”这个前提不自动推出“我可以把字体发给所有访客”。

这个站为此换过一次字体,经过写在开张那篇里。

排版参照 kami

菜单关闭