Web 字体格式的本质差异、浏览器渲染机制与性能优化
一句话摘要
TTF 是桌面时代的本地字体格式(未经压缩,单个文件可达 18MB+),WOFF2 是为 Web 传输优化的压缩格式(压缩率 60-70%)。在网站上使用未经压缩的 TTF 是字体无法加载、浏览器标签持续转圈的最常见原因。转换方法:woff2_compress font.ttf。
本站(note.makoto.top)上线后出现一个奇怪现象:页面内容正常显示,所有文字都能看到,但浏览器标签页一直在转圈"加载中",始终不显示完成状态。打开开发者工具 Network 面板,看到十几个字体文件请求,每个都很大、下载很慢。
问题就出在字体格式上。
当时使用的是 Maple Mono CN(一款含中文的等宽字体),共引用了 16 个字重/样式变体,每个都是 TTF 格式,单个文件约 18MB。浏览器需要下载这些文件才能完成字体加载,即使在有 font-display: swap 的情况下(文字可以先用备用字体显示),浏览器仍然在后台持续下载这些大文件,标签页就一直显示加载中。
这篇文章就从这次排查出发,讲清楚 TTF 和 WOFF2 的区别、为什么 Web 上必须用 WOFF2、以及浏览器加载字体的整个流程。
直觉类比:字体格式就像图片格式。BMP 是未压缩的原始位图(文件巨大但信息完整),JPEG 是有损压缩(文件小、肉眼看不出差异)。TTF 相当于字体的 "BMP"——未经 Web 优化的原始格式;WOFF2 相当于字体的 "JPEG"——专门为网络传输压缩过的格式。
字体文件本质上是一个矢量图形数据库,里面存储了每个字符的轮廓描述(用贝塞尔曲线定义的形状)、字距调整信息、以及各种排版元数据。对于拉丁字母(26 个字母 + 数字 + 符号),这个数据库很小(通常几百 KB)。但对于中文字体,由于字符集包含数万个汉字(GB2312 约 6,763 字,GB18030 约 70,000+ 字),字体文件体积天然巨大。
为什么中文字体特别大?
英文只有 26 个字母,每个字母的轮廓数据量很小,整个字体可能只有 200-500 KB。
中文常用字 3,500+,完整字符集约 20,000-30,000 个字形(Glyph)。每个字都是独立的矢量轮廓——想象一下你要为每个汉字画一幅微小的矢量图并存储所有曲线坐标。这就是为什么 Maple Mono CN 单个字重就达到 18MB。
字体格式经历了从桌面到 Web 的完整演进。理解这段历史有助于理解为什么 WOFF2 是今天 Web 字体的标准答案。
Apple 开发、后授权给 Microsoft。使用二次贝塞尔曲线描述字形轮廓,内含 truetype 指令(hinting)用于低分辨率屏幕的像素级渲染优化。至今仍是桌面操作系统最通用的字体格式。
Microsoft 和 Adobe 联合开发,在 TrueType 基础上扩展。核心改进:支持 PostScript 轮廓(三次贝塞尔曲线)、高级排版特性(连字、小型大写字母、替代字形等)。OTF 是 TTF 的超集,容器内可以装 TrueType 或 PostScript 轮廓。
Mozilla、Opera、Microsoft 联合制定,专为 Web 设计。本质上是在 TTF/OTF 数据外包了一层轻量压缩(使用 zlib),并加入了元数据字段(字体来源、授权信息)。压缩率约 30-40%。
Google 主导开发的第二代 Web 字体格式。将压缩算法从 zlib 升级为 Brotli,压缩率提升到 50-70%。它不做全文件压缩,而是对字体内部的每个数据表(glyf、loca、cmap 等)分别压缩,解码时不需要解压整个文件。如今所有现代浏览器均完整支持。
TTF 的定位是操作系统级字体。它被设计为安装在本地磁盘上,由操作系统直接加载使用,不存在网络传输场景,因此没有任何压缩设计。把它直接放在 Web 上,相当于让浏览器下载一个设计给本地用的未压缩二进制文件。
TTF 内部采用二次贝塞尔曲线描述字形。这种曲线在数学上更简单、渲染更快,但在描述复杂曲线时需要更多控制点(文件体积更大)。
OTF 可以理解为"TTF 的扩展容器"。它向后兼容 TrueType,同时引入了:
!= → ≠)。OTF 同样没有为 Web 传输做任何优化,仍然不适合直接用于网站。
TTF vs OTF — 简单记忆法
TTF 是 Apple 做给屏幕显示的(二次曲线,hinting 强大);OTF 是 Adobe 做给印刷出版的(三次曲线,排版特性丰富)。在 Web 场景下,两者都要被压缩成 WOFF2 使用,原始格式的差异对网站加载没有影响。
WOFF 的核心思想很简单:在已有 TTF/OTF 数据外面套一层压缩壳。它使用 zlib 对字体表进行压缩,并添加了 XML 元数据块(用于存储字体来源、授权、版权信息)。
WOFF 不是新字体格式,而是一种容器格式。解压后就是一个完整的 TTF/OTF 文件。这种设计让浏览器实现成本极低——拿到 WOFF 文件 → 解压 → 得到标准 TTF/OTF → 用已有的字体引擎渲染。
WOFF 诞生于 2009 年,那一年 `
WOFF2 是 Google 在 2013-2018 年间主导开发的下一代 Web 字体格式。它的核心改进是全面换用 Brotli 压缩算法,并采用了更激进的"表级压缩"策略。
和 WOFF(zlib 压缩)相比,Brotli 的优势体现在三个层面:
Brotli 使用更大的滑动窗口(最大 16MB vs zlib 的 32KB)和预定义词典,对重复模式数据(字体中大量重复的结构)压缩效果显著优于 zlib
不是把整个字体当一个大 blob 压缩,而是把内部的 glyf、loca、cmap 等表各自独立压缩。解码时按需解压,不需要先解压全部
Brotli 解码速度约为同等压缩率下 zlib 的 2 倍。且浏览器内建 Brotli 解码器(HTTP Content-Encoding 就用它),零额外开销
如今 WOFF2 的浏览器支持率已经达到 98%+(截至 2026 年,Chrome、Firefox、Safari、Edge 所有版本均完整支持),可以放心作为唯一字体格式使用。
WOFF2 的压缩为什么比 WOFF 强这么多?原因不在某个单点技巧,而是一系列字体领域知识驱动的定制优化的叠加。
字体文件内部由多个独立的"表"(Table)组成:
| 表名 | 作用 | 占体积比(中文字体) |
|---|---|---|
glyf | 每个字形的矢量轮廓数据(贝塞尔曲线坐标数组) | ~70% |
loca | 每个字形在 glyf 表中的偏移索引 | ~5% |
cmap | Unicode 码点 → 字形编号的映射表 | ~10% |
hmtx | 水平度量信息(字宽、左右留白) | ~8% |
name | 字体名称、版权等元数据 | ~2% |
| 其他 | hinting、kerning、GSUB、GPOS 等 | ~5% |
WOFF 的做法是先把所有表拼成一个整体,然后用 zlib 压缩整个文件——这种做法简单粗暴,对不同类型的数据一视同仁。
WOFF2 的做法是对每种表用针对性的策略处理:
类比理解
WOFF 的做法是把厨房、书房、衣柜的所有物品混在一起装进一个箱子——简单但空间利用率低。
WOFF2 的做法是先把衣服叠好放真空压缩袋(glyf 增量编码)、把书按类别装箱(cmap 范围编码),再把所有处理过的物品放进用高效材料做的箱子(Brotli 整体压缩)。每一步都在用领域知识节约空间。
中文字体中有大量结构相似的字形。例如"木"和"林"和"森"共享同一个偏旁,"氵"旁的字有数百个。Brotli 的滑动窗口可以捕捉到这些跨字形的重复模式,实现远超通用压缩的效果。这也是为什么 Maple Mono CN 从 18MB → 5.3MB(压缩率 70%)——中文特有的字形冗余反而成为了压缩的"帮手"。
字体是浏览器的延迟加载资源——浏览器不会预先下载所有字体,而是等到 CSS 解析到 @font-face 并且实际有文字使用了该字体时才开始下载。这个机制本身是好的(避免下载用不到的字体),但它也带来了字体加载时序上的复杂性。
Step 3 是一个关键细节:浏览器只在文本真正使用了某个字重/样式时才下载对应字体。也就是说,如果你的 CSS 声明了 10 种字重,但页面上只用了 Regular (400),理论上浏览器只会下载 Regular 这一个文件。
但在实际排错中发现,浏览器对于大字体的处理更"激进"——当字体文件过大(18MB TTF),即使只使用了一个字重,下载过程本身就会占据网络线程,使得浏览器一直处于"等待资源"状态,标签页持续显示加载中。
font-display 是 @font-face 中的一个 CSS 属性,控制在字体加载期间文字的渲染策略。理解它对于诊断"文字明明显示了但页面还在加载"的问题至关重要。
| font-display 值 | 字体加载期间 | 加载失败后 | 适用场景 |
|---|---|---|---|
auto |
由浏览器决定(各浏览器默认策略不同) | — | 不推荐,行为不可控 |
block |
隐藏文字(最多 3 秒),白屏等待 | 使用备用字体 | 品牌页面,字体是视觉核心 |
swap |
立即显示备用字体,下载完后替换 | 保持备用字体 | 通用场景,本站使用 |
fallback |
等待很短时间(约 100ms),之后显示备用字体 | 保持备用字体 | 偏重性能的场景 |
optional |
等待极短时间,网络差时直接放弃加载 | 不加载,只用备用字体 | 对字体容忍度极高的场景 |
本站使用 swap——文字先用系统等宽字体显示,WOFF2 下载完成后无缝替换。这解释了为什么之前用 TTF 时"文字能看到但标签页还在转":swap 让文字立即渲染,但 18MB 的 TTF 文件下载时间太长,浏览器一直没等到资源完成。
FOUT(Flash of Unstyled Text)和 FOIT(Flash of Invisible Text)
FOUT:先用备用字体显示,自定义字体加载后替换 → 用户能看到"字体突然变了"的一瞬间。这是 swap 的效果。
FOIT:自定义字体加载完成前文字完全不可见 → 用户看到的是空白区域。这是 block/auto 在某些浏览器的默认行为。
对笔记/文档类网站,FOUT 好于 FOIT——用户能立即开始阅读,字体切换的瞬间不太会注意到。本站选择 swap 就是基于这个设计决策。
以下是本站字体优化的完整记录,可以作为类似问题的参考流程。
# 1. 安装 woff2 压缩工具(macOS)
$ brew install woff2
# 2. 批量转换所有 TTF 为 WOFF2
$ for f in *.ttf; do
woff2_compress "$f"
done
# 3. 查看压缩效果
$ ls -lh *.woff2
# MapleMono-CN-Regular.woff2 → 5.1MB(原来 18MB)
# 总大小:288MB → 84MB,压缩率约 70%
/* 4. 更新 CSS:TTF → WOFF2 */
/* 优化前 */
@font-face {
font-family: 'Maple Mono CN';
src: url('../.fonts/MapleMono-CN-Regular.ttf') format('truetype');
font-weight: 400;
font-display: swap;
}
/* 优化后 */
@font-face {
font-family: 'Maple Mono CN';
src: url('../.fonts/MapleMono-CN-Regular.woff2') format('woff2');
font-weight: 400;
font-display: swap;
}
# 5. 清理 git 仓库中的大文件
$ git rm --cached .fonts/MapleMono-CN-unhinted/*.ttf
$ echo "*.ttf" >> .gitignore # 不再追踪 TTF 源文件
$ git add .fonts/MapleMono-CN-unhinted/*.woff2
$ git commit -m "perf: 字体 TTF → WOFF2,288MB → 84MB"
要不要同时提供 TTF + WOFF2 兼容写法?
CSS @font-face 的 src 支持多个 url(),可以从 TTF 到 WOFF2 依次降级。但 2026 年 WOFF2 浏览器支持率已达 98%+,提供 WOFF2 单一格式完全够用。多格式兼容反而会增加 CSS 代码量和用户首次访问时可能下载错误格式的概率。
无论什么场景,Web 上发布的字体必须转换为 WOFF2 格式。TTF 是桌面格式,不应该出现在 HTTP 响应中。这个结论适用于任何字体——中文、英文、等宽、衬线,没有例外。
WOFF2 能把 18MB TTF 压到 5MB,但如果你的网站总共只用了 2,000 个汉字(例如技术博客的常用字集),那么一个包含 20,000+ 字的完整字体是巨大的浪费。
子集化就是从完整字体中提取你实际需要的字符,生成一个只包含这些字符的迷你字体。工具推荐:
pyftsubset(fontTools 套件):Python 命令行工具,灵活度高subsetting.xyz:在线工具,上传字体 + 输入需要的文字即可对于本站的 Maple Mono CN,目前暂未做子集化,因为笔记仓库是增量更新的,新文章可能引入新汉字,子集维护成本较高。5MB 的 Regular 字重在现代网络条件下加载体验可接受。
子集化类比
完整字体 = 一本《辞海》(所有字都在,但很重)。
子集化字体 = 从《辞海》里把你需要的那几页复印下来装订成册——内容一样,体积只有原来的几十分之一。
本站声明了 Thin (100) 到 ExtraBold (800) 共 10 个字重,每份 WOFF2 约 5MB。在实际渲染中,大多数页面只会用到 Regular (400)、Bold (700) 两种字重。其他 8 份字体声明虽然不会被下载(浏览器按需加载),但 @font-face 声明本身不消耗带宽——关键不是声明了多少,而是页面上实际用了多少字重。
但如果你发现自己确实只用少量字重,就应该精简声明——减少 CSS 中的无用规则,降低维护复杂度。
字体文件相比其他静态资源有很大的特殊性:几乎不会改变。一旦字体选定了,可能数月甚至数年不变。因此可以设置比 JS/CSS 更长的缓存时间:
# 字体文件缓存 30 天
location ~* \.(woff|woff2)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
immutable 告诉浏览器"这个文件永不改变",浏览器即使在硬刷新时也不会重新请求该资源。本站当前配置的是 7 天缓存,未来可以适当延长。
如果 Regular 字重是页面核心字体的第一优先级,可以在 HTML 中用 <link rel="preload"> 提前告诉浏览器"这个字体很重要,尽早下载":
<link rel="preload"
href="/.fonts/MapleMono-CN-Regular.woff2"
as="font"
type="font/woff2"
crossorigin>
crossorigin 是必须的——即使同域加载,字体 preload 也需要这个属性,这是浏览器规范要求的。本站暂未使用 preload,因为 font-display: swap 已经保证了首屏文字的即时可见,preload 的边际收益不大。
| 维度 | TTF | WOFF2 |
|---|---|---|
| 诞生年份 | 1991 | 2018 |
| 设计目标 | 桌面操作系统本地字体 | Web 网络传输 |
| 压缩算法 | 无 | 表级定制处理 + Brotli |
| 典型大小(中文字体单字重) | 18MB | 5MB |
| 浏览器支持 | 全部支持(但不该用) | 98%+(2026) |
| 适合场景 | 本地安装、桌面排版 | 一切 Web 场景 |
核心认知:
brew install woff2 → woff2_compress font.ttf,一条命令完成。