中文 Web 字体瘦身:把 15 MB 思源黑体压到 8 KB

· 约 5 分钟 🅰 字体子集化

中文 Web 字体长期是性能优化的痛点——10 MB 起步的字库往 CDN 一扔,首屏要么白屏要么字体闪一下。子集化是唯一靠谱的解法——只留页面实际用到的字。这篇带你完整走一遍:从原字体到上线 8 KB 的子集 + 完整 CSS 配置。

为什么中文字体那么大

英文字体只需要覆盖 ASCII 95 个字符 + 一些扩展拉丁,几百 KB 足够。中文要覆盖几万个汉字

字符集字数思源黑体单字重大小
ASCII95< 1 KB
GB2312 一级3755~800 KB
GB2312 全集6763~1.5 MB
GB18030 基本集27484~6 MB
GB18030 全集70244~10-15 MB
含扩展 ABCD90000+~15-20 MB

每个汉字平均要 100-300 字节存储字形数据(笔画越多越大)。GB2312 已经够覆盖中文 99% 日常用字。

子集化原理

字体文件是若干个(table):

内容
cmap字符 → 字形 ID 的映射
glyf / CFF字形数据(曲线 / 笔画)
head / hhea字体元数据
name字体名字符串
hmtx字符宽度
kern / GPOS字距调整

子集化做的事——

  1. 解析原字体所有表
  2. 按指定字符列表筛 cmap
  3. 只保留对应的 glyf 字形数据
  4. 重新生成 head / hmtx 等关联表
  5. 重新打包成新字体文件

字符越少 → glyf 表越小 → 字体文件越小。50 字 vs 7000 字,体积差 30-100 倍。

完整案例:品牌网站 Hero 标题

需求:landing page 的 H1 用思源宋体,文字内容固定。

<h1 class="hero-title">
  让每一份代码都简洁有力
</h1>

字符集 = 13 个汉字。我们做最小化子集。

步骤 1:收集字符

让每一份代码都简洁有力

加上备用——常见标点 + 数字字母兜底:

让每一份代码都简洁有力。,:;!?""''0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ

总计约 90 字符。

步骤 2:跑子集化

打开 字体子集化

  1. 拖入 SourceHanSerifSC-Bold.otf(约 14 MB)
  2. 选”子集化”模式
  3. 粘贴上面 90 字符
  4. 输出格式选 WOFF2
  5. 下载得到 SourceHanSerifSC-Bold.subset.woff2(约 12 KB)

14 MB → 12 KB,压缩比 1166 倍

步骤 3:CSS 配置

@font-face {
  font-family: "SourceHanSerif";
  src: url("/fonts/source-han-serif.subset.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+4E00-9FFF, U+0020-007E, U+3000-303F;
}

.hero-title {
  font-family: "SourceHanSerif", "Source Han Serif SC", "Songti SC", serif;
  font-weight: 700;
  font-size: 48px;
}

关键配置说明

  • format("woff2") ——告诉浏览器这是 WOFF2 格式
  • font-display: swap ——加载期间立即用兜底字体显示,到位后切换。SEO 关键——避免空白期,Lighthouse 加分
  • unicode-range ——只对汉字 / ASCII / CJK 标点生效,避免英文段落不必要触发字体加载
  • fallback ——子集字体没覆盖的字符 fallback 到系统宋体

font-display 的取舍

期间显示字体到位后用途
block空白(FOIT)3 秒切换默认,体验差
swap兜底字体(FOUT)切换正文首选
fallback短 FOIT 100ms + 兜底3 秒内切换折中
optional短 FOIT 100ms + 兜底不切换装饰字体

实战推荐

  • 正文 / 标题 → swap(内容立刻可读,体验最好)
  • 装饰性字体(特殊艺术字) → optional(避免布局抖动)
  • 别用 block —— 用户被迫看 3 秒空白,Web Vitals 大幅扣分

用 unicode-range 拆分大字体

正文场景需要覆盖大量汉字时,按区段切多个子集

/* ASCII */
@font-face {
  font-family: "Noto Sans SC";
  src: url("/fonts/noto-ascii.woff2") format("woff2");
  unicode-range: U+0020-007F;
  font-display: swap;
}

/* 常用 GB2312 */
@font-face {
  font-family: "Noto Sans SC";
  src: url("/fonts/noto-gb2312-common.woff2") format("woff2");
  unicode-range: U+4E00-7FFF;
  font-display: swap;
}

/* 扩展汉字 */
@font-face {
  font-family: "Noto Sans SC";
  src: url("/fonts/noto-extended.woff2") format("woff2");
  unicode-range: U+8000-9FFF, U+3400-4DBF;
  font-display: swap;
}

浏览器只下载页面实际用到字符所在的子集——一篇主要常用字的文章只下载第二个 50 KB,不会触发扩展汉字下载。这是 Google Fonts Noto Sans SC 的官方策略。

FOIT vs FOUT 的视觉效果

FOIT(block):[空白空白空白] → [字体到位,正常显示]
                ↑ 用户看不到字 3 秒

FOUT(swap): [兜底字体显示] → [切换到目标字体]
                ↑ 字体可能跳一下,但内容立刻可读

FOUT 的”跳一下”叫做 CLS(Cumulative Layout Shift)——如果兜底字体和目标字体的字宽差异大,会触发布局抖动。减轻方法

.hero-title {
  font-family: "TargetFont", system-ui, sans-serif;
  font-size-adjust: 0.5;     /* 兜底字体高度对齐 */
}

或用 size-adjustascent-override 等 @font-face descriptor 精细对齐字宽。

子集化的盲点:动态内容

子集化不适合字符不可控的场景:

  • 用户输入(搜索框、评论、UGC)
  • 动态数字(订单号、阅读量、价格)
  • AI 生成内容
  • 多语言切换

应对策略——

  1. 关键内容子集 + 全字体回退:标题 / 品牌字用子集字体;正文用系统字体
  2. 拆分加载:业务核心字符提前子集;UGC 不强制特殊字体
  3. 保留 ASCII 全集:子集里至少含 0-9 a-z A-Z + 常见标点,避免动态数字 / 字母豆腐

何时用浏览器工具,何时用 pyftsubset

场景工具
一次性瘦身 / 试错浏览器工具
商业字体不能上传浏览器工具
设计师手动操作浏览器工具
集成到 CI / build pipelinepyftsubset
可变字体 instancingpyftsubset
几十个字体批量处理pyftsubset
需要精细控制(kerning / hinting)pyftsubset

两者互补——浏览器工具确定子集策略,确定后写进构建脚本用 pyftsubset 自动化。

上线 checklist

  • ✅ 字体格式 WOFF2(不要传 TTF / OTF 上线)
  • font-display: swap 避免空白期
  • unicode-range 限制生效字符范围
  • ✅ fallback 字体链 system-ui, sans-serif 兜底
  • ✅ 子集包含 ASCII + 常用标点防豆腐
  • ✅ HTTP 头 Cache-Control: max-age=31536000, immutable 强缓存
  • ✅ Lighthouse 跑分确认 LCP / CLS 没扣分
  • ✅ 商业字体看 EULA 确认子集化合规

配套工具

中文 Web 字体不是不能用,是要会用。10 MB 整包上线是反模式,子集 + WOFF2 + font-display 三件套是工业标准。

❓ 常见问题

为什么不能直接把 10 MB 字体上 CDN 让浏览器缓存?

首次访问惩罚太大——10 MB 字体在 4G 网络下载约 8-15 秒,期间用户要么看到无字(FOIT)要么看到默认字体(FOUT),首屏体验很糟糕。CDN 缓存只对二次访问有效——首次访问的成本是不可避免的,而搜索引擎评分(Core Web Vitals 的 LCP)也是按首次访问算的。正确做法——子集化 + 拆分:核心标题字(10-30 字)打包 8-15 KB 几乎瞬间到位;正文内容按需 unicode-range 拆分到 30-50 KB 的多个分片;用户访问哪页加载哪页的字。10 MB 字体永远不应整包上线,除非是字体网站本身。

font-display 几个值具体差别是什么?

控制字体加载期间的显示策略——(1) auto:浏览器决定,多数实现等同 block;(2) block:先显示空白(FOIT),最长 3 秒等字体;3 秒后还没到位用兜底字体;字体到位后切换。用户看到 3 秒空白,体验差;(3) swap:立刻用兜底字体显示(FOUT),字体到位后切换。用户看到字体闪一下,但内容立即可读——SEO / Web Vitals 友好;(4) fallback:100ms 内不显示字(短 FOIT),100ms 后用兜底,3 秒内字体到位才切换;(5) optional:100ms 内不显示,之后不再切换。经验法则——正文用 swap(用户看得到字),装饰字体用 optional(避免布局抖动)。

我怎么知道页面上用了多少字?

两种方法——(1) 手动收集:把页面上所有用到该字体的元素文本复制出来,排重。适合页面少 + 字符可控的场景。(2) 自动扫描:build 时跑脚本扫所有源文件(HTML / Markdown / JSX / Vue),grep 出字符并去重。Chrome DevTools 工作流——document.querySelectorAll("h1, h2, .brand").map(el => el.textContent).join("") 拿到所有用了该字体的文本。保守做法——除了实际用到的字符,再加上常用标点(。,、;:?!""'「」()—— …)和数字字母 0-9 a-z A-Z 一些备用,避免新增内容时立刻挂掉。这套通常 50-100 字符以内,子集 10-20 KB。

unicode-range 怎么拆分中文字体?

按 Unicode 区段切片——浏览器只下载页面实际用到字符所在的子集。标准切法——(1) ASCII U+0000-007F;(2) 中日韩统一表意 U+4E00-9FFF(最常用 2 万字);(3) 扩展 A U+3400-4DBF(次常用);(4) 扩展 B-F(罕见)。实操更细——可以按 GB2312 一二级(约 6700 字)单独打包,覆盖 99% 业务文本;扩展字另外加载。配置示例——多个 @font-face 共用一个 font-family,每个指定不同的 unicode-range,浏览器会自动按页面字符选择哪个分片下载。Google Fonts 的 Noto Sans SC 就是这么切的——一个字体名背后是 100+ 子集分片。

我用 fonttools / pyftsubset 也能做,为什么用浏览器工具?

浏览器工具优势——(1) 零安装:不用配 Python 环境 / pip install / 处理依赖冲突;(2) 隐私:商业字体许可禁止上传到第三方 SaaS 时合规;(3) 设计师可用:不会命令行的 UI 设计师也能跑;(4) 临时性:试不同字符集看体积变化,迭代快。fonttools / pyftsubset 优势——(1) 嵌入构建脚本:每次 deploy 自动重生成;(2) 处理超大字体(GB18030 全集)更稳;(3) 可变字体 instancing;(4) 精细控制 OS/2 表 / hinting / kerning。实务——团队网站用浏览器工具试好策略,确定字符集后写进 CI 用 pyftsubset 自动化。两者互补不冲突。

子集化后字体显示个别字"豆腐块"是怎么回事?

子集没覆盖到那个字——浏览器找不到字形数据,显示成"⬜"或方框,俗称"豆腐块"。常见原因——(1) 标点漏了:保留了汉字但忘了"。,!?";(2) 数字漏了:动态显示数字(如阅读量、价格)字体子集没含 0-9;(3) 用户输入:搜索框 / 评论 / UGC 内容字符不可控。修复——(1) 子集保留 ASCII 全集(95 个 + 数字字母约 1-2 KB 开销);(2) 加常用标点 30-50 个;(3) 用 unicode-range + 兜底字体——本字体未覆盖的字符自动 fallback 到 system-ui,至少不出豆腐。

子集化的字体二次分发有版权问题吗?

取决于原字体许可——(1) 开源字体(思源系列 SIL OFL、Google Noto):可任意子集化、修改、二次分发,仅需保留版权声明;(2) 商业字体(方正 / 汉仪 / 蒙纳):通常禁止未经授权的修改和二次分发——子集化属于修改字体文件,理论上需要授权许可。许多商业字体许可允许 web 嵌入但要求向供应商报备域名 / 流量;(3) 付费购买的网页字体许可证:通常允许子集化但禁止把字体托管到第三方供他人使用。实操建议——商业字体先看 EULA,免费字体(思源 / Google Fonts)随便子集化;不确定时联系字体供应商。

🅰 打开 字体子集化 TTF/OTF/WOFF/WOFF2 互转·按文本提取字形·中文字体十几 MB 压到几 KB·HarfBuzz/Google woff2 本地处理不上传