一句话说明白
字体子集化(Subsetting)和格式转换就像是出门度周末打包行李:你不需要拖着一个塞满 6000 个生僻西里尔文和奇葩符号的巨型行李箱,而是把你确切需要的那几个字母塞进一个极其压缩、专为 Web 定制的轻量级背包里。
解决什么痛点
如果你是在 2005 年写网页,你的排版选择基本就是一场绑架案。你只有 Arial, Times New Roman, Georgia, Verdana 可以选,要是想报复社会,可能还会用一下 Comic Sans。这些被称为“Web 安全字体”——意思是我们只能双手合十,祈祷用户的操作系统里已经装了这些字体。
接着,@font-face 革命爆发了。突然之间,设计师可以嵌入任何他们想要的自定义字体。Web 变得无比漂亮,但也变得奇慢无比。
标准的桌面字体(TrueType 或 OpenType)是为印刷和通用语言支持而设计的。一个 .ttf 文件可能包含成千上万个 glyphs(字形),支持英语、希腊语、越南语、特殊的数学符号,以及 20 种不同花样的 & 符号。这对于 Word 软件来说很棒,但对于拿着破 3G 网络刷网页的浏览器来说就是一场灾难。下载一个 400KB 的字体文件会阻塞渲染,导致让人绝望的“无样式文本闪烁”(FOUT),甚至更惨的“不可见文本闪烁”(FOIT)。
要治好这种“排版肥胖症”,我们需要两副药:
- 专门为 Web 设计的更好的压缩算法(于是诞生了 WOFF 和 WOFF2 格式)。
- 字体子集化(Subsetting),也就是像做外科手术一样,把特定网站根本用不到的字符和语言表统统切除。
底层逻辑是怎样的
要理解我们怎么把字体变小,你得先弄明白字体文件到底是个啥。现代字体(比如 TTF 或 OTF)可不只是一个装着一堆矢量图的文件夹。它是一个极其复杂的、基于 SFNT 容器格式的二进制数据库。
字体的解剖学
在 SFNT 容器内部,数据被组织在不同的表中:
glyf或CFF:实际的矢量图(字母的线条和曲线)。cmap:字符映射表。它负责把 Unicode 码位(比如U+0041代表 'A')翻译成glyf表里具体的矢量图。GSUB/GPOS:高级 OpenType 特性,比如连字(ligatures,比如把 'f' 和 'i' 连起来)和字距调整(kerning,特定字母对之间的间距)。name:元数据,包含字体名称、版权信息和设计师资料。
子集化的魔法
当你对字体进行子集化处理时,解析器会撬开这个 SFNT 容器。你扔给它一个字符列表——比如,只保留基础的英文字母、数字和常用的标点符号。
解析器可不是简单地去 glyf 表里删掉没用的矢量图就算了。它必须一丝不苟地重建整个数据库。它要重写 cmap 表来剔除被抛弃的 Unicode 码位;它还要梳理 GSUB 和 GPOS 表,扒掉那些已经不存在的字母的字距调整信息,保证文件不会坏死。最后,它还要重新计算文件 header 的内存偏移量(memory offsets)。
一个 300KB 的标准字体,如果仅仅抽取基础拉丁字符,轻轻松松就能暴瘦到 15KB。
格式之争:WOFF vs WOFF2
字体子集化搞定后,接下来就是压缩环节了。
| 格式 | 压缩技术 | 结论 |
|---|---|---|
| TTF / OTF | 无(通常) | 桌面设计软件的最爱,Web 分发的灾难。 |
| WOFF | Zlib | 初代 Web 字体开放格式。好用,但过气了。 |
| WOFF2 | Brotli | 现代标准。先转换字体表结构,再进行极其残暴的压缩。 |
WOFF2 是无可争议的 Web 字体之王。它可不是简单地把一个 TTF 文件给 zip 一下。WOFF2 在物理层面转换了 SFNT 表——具体来说就是把相似的数据类型归拢到一起——这样底层的 Brotli 压缩算法就能找到更长的重复模式。结果就是,它的文件体积比 WOFF1 小了最多 30%,如果再配合上子集化,甚至能比原始 TTF 小 95%。
用 CSS 串联起来
一旦你拿到了高度压缩、经过子集化处理的 WOFF2 文件,你就可以用 CSS 把它交代给浏览器了。这里的核心是 unicode-range 属性。它等于是在跟浏览器说:“只有当页面上真出现了属于这个范围的字符时,你才去下载这个文件。”
@font-face {
font-family: 'MyCustomFont';
/* Load the highly compressed WOFF2 format */
src: url('mycustomfont-subset.woff2') format('woff2');
/* Only trigger the download if the page uses basic Latin characters */
unicode-range: U+0000-00FF, U+0131, U+0152-0153;
font-display: swap;
}
真实世界的翻车与自救
“就为了个 & 符”引发的血案
一家精品设计机构在给某高端餐厅做落地页。大标题的排版看起来美炸了,主要原因是在餐厅名字 "Salt & Ash" 中,他们给 & 符号单独用了一款华丽的草书字体。
然而,Lighthouse 的性能评分一路红灯。开发小哥一看,好家伙,为了渲染这么一个单独的字符,居然逼着用户下载了 250KB 的 TTF 文件。通过把字体扔进子集化工具里,仅仅把 & 符号(Unicode U+0026)抠出来,他们生成了一个轻到令人发指的 WOFF2 文件,大小只有 800 字节(bytes)。
**经验教训:**永远别为了装饰性的首字母下沉、图标或者单个字符,去加载整套完整的字体。要狠下心来搞子集化(Subset aggressively)。
多语种带来的 MB 级灾难
一家 SaaS 公司在为日韩用户做官网本地化。为了保持品牌调性一致,他们嵌入了一款业内口碑极佳的 CJK(中日韩)字体。大家都懂,CJK 字体以体积庞大著称,因为里面塞了成千上万个表意文字。这个文件足足有 4.5MB。
结果,他们的欧美用户突然发现页面文本要等好几秒才能刷出来,因为浏览器在拼命下载这 4.5MB 他们这辈子都看不见的亚洲字符。工程团队的解法是:将这个巨无霸母体字库分割成多个子集“块”(一个给基础拉丁字母的 WOFF2,一个给平假名/片假名等),然后用带严格 unicode-range 描述符的 @font-face 块来部署。现在,欧美用户的浏览器只会乖乖下载那 15KB 的拉丁字母块。
**经验教训:**善用子集化和 unicode-range,根据用户的实际语言需求动态按块(chunks)加载字体。
常见的坑与反面教材
- 手滑把空格给删了: 当开发人员手动输入想保留的字符(比如
abcdefghijklmnopqrstuvwxyz)时,经常会忘记加上空格字符(U+0020)。没有空格,你精心定制的字体在每次遇到单词间隙时,都会 fallback 退化成 Arial,导致页面看起来像一封惊悚的勒索信。 - 非要伺候上古格式: 很多开发者至今还在 CSS 里生成并提供 EOT、TTF 和 SVG 字体,美其名曰“为了保险起见”。别闹了。全球 97% 以上的浏览器都支持 WOFF2。只给 WOFF2 就行了,顶多再提供个 WOFF 给远古浏览器兜底,其他的赶紧扔了吧。
- 一刀切砍掉关键的 OpenType 特性: 一些激进的子集化工具会诱惑你砍掉
GSUB和GPOS表,就为了省那几 KB 的空间。如果你的字体非常依赖复杂的连字(比如把->变成一个箭头),或者得靠字距调整(kerning)才不会丑得辣眼睛,那删掉这些表绝对会毁掉你的排版。 - 忘了加
font-display: swap: 哪怕是一个只有 12KB 的子集化 WOFF2,也需要几毫秒来下载。如果你没在 CSS 里加上font-display: swap,现代浏览器会在字体加载完之前彻底隐藏你的文本。给用户点系统默认字体看看吧,别让他们盯着白板干等。
为什么你需要关注它
说到毁掉 Web Vitals 核心性能指标,Web 字体在历史上绝对是罪魁祸首之一。如果你在乎 LCP(最大内容绘制) 或 CLS(累积布局偏移),你就必须重视字体分发。
当你从设计师那里拿到,或者从字库下载到一个 .ttf 或 .otf 文件的那一刻起,你就该考虑字体的子集化和转换了。在 Web 上直接硬塞未经处理的桌面字体,简直就跟用一张未压缩的 10MB 位图去替代 WebP 一样离谱。
通过转换为 WOFF2 并剔除掉你根本用不到的数千个字形(glyphs),你能大幅节省带宽成本,降低应用的碳排放,并且确保你的网站在用户点击链接的几毫秒内就是实际可读的。
延伸阅读
- MDN Web Docs: @font-face - 在 CSS 中如何正确引入自定义字体和使用
unicode-range的绝对圣经。 - W3C WOFF2 Specification - 官方技术规范,详细阐述了 WOFF2 是如何转换并压缩 SFNT 表的。
- RFC 7932: Brotli Compressed Data Format - IETF 官方文档,揭秘赋予 WOFF2 超能力的 Brotli 压缩算法。
- Web.dev: Optimize WebFonts - Google 官方出的权威指南,教你如何最小化 Web 字体体积并防止布局偏移。
- Google Fonts Knowledge: What is a font? - 深入探讨 SFNT 表的结构,以及字形(glyphs)到底是怎么存储在底层的。