TTF 与 WOFF2

Web 字体格式的本质差异、浏览器渲染机制与性能优化

未分类 · 2026 年 6 月 · 基于一次实际网站字体加载卡顿问题的排查与修复

一句话摘要

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 字体的标准答案。

1991
TrueType(TTF)

Apple 开发、后授权给 Microsoft。使用二次贝塞尔曲线描述字形轮廓,内含 truetype 指令(hinting)用于低分辨率屏幕的像素级渲染优化。至今仍是桌面操作系统最通用的字体格式。

1996
OpenType(OTF)

Microsoft 和 Adobe 联合开发,在 TrueType 基础上扩展。核心改进:支持 PostScript 轮廓(三次贝塞尔曲线)、高级排版特性(连字、小型大写字母、替代字形等)。OTF 是 TTF 的超集,容器内可以装 TrueType 或 PostScript 轮廓。

2009
WOFF(Web Open Font Format)

Mozilla、Opera、Microsoft 联合制定,专为 Web 设计。本质上是在 TTF/OTF 数据外包了一层轻量压缩(使用 zlib),并加入了元数据字段(字体来源、授权信息)。压缩率约 30-40%。

2018
WOFF 2.0(WOFF2)

Google 主导开发的第二代 Web 字体格式。将压缩算法从 zlib 升级为 Brotli,压缩率提升到 50-70%。它不做全文件压缩,而是对字体内部的每个数据表(glyf、loca、cmap 等)分别压缩,解码时不需要解压整个文件。如今所有现代浏览器均完整支持

TrueType(TTF)— 桌面字体鼻祖

TTF 的定位是操作系统级字体。它被设计为安装在本地磁盘上,由操作系统直接加载使用,不存在网络传输场景,因此没有任何压缩设计。把它直接放在 Web 上,相当于让浏览器下载一个设计给本地用的未压缩二进制文件。

TTF 内部采用二次贝塞尔曲线描述字形。这种曲线在数学上更简单、渲染更快,但在描述复杂曲线时需要更多控制点(文件体积更大)。

OpenType(OTF)— TTF 的进化版

OTF 可以理解为"TTF 的扩展容器"。它向后兼容 TrueType,同时引入了:

OTF 同样没有为 Web 传输做任何优化,仍然不适合直接用于网站。

TTF vs OTF — 简单记忆法

TTF 是 Apple 做给屏幕显示的(二次曲线,hinting 强大);OTF 是 Adobe 做给印刷出版的(三次曲线,排版特性丰富)。在 Web 场景下,两者都要被压缩成 WOFF2 使用,原始格式的差异对网站加载没有影响。

WOFF — Web 字体的第一次尝试

WOFF 的核心思想很简单:在已有 TTF/OTF 数据外面套一层压缩壳。它使用 zlib 对字体表进行压缩,并添加了 XML 元数据块(用于存储字体来源、授权、版权信息)。

WOFF 不是新字体格式,而是一种容器格式。解压后就是一个完整的 TTF/OTF 文件。这种设计让浏览器实现成本极低——拿到 WOFF 文件 → 解压 → 得到标准 TTF/OTF → 用已有的字体引擎渲染。

WOFF 诞生于 2009 年,那一年 `

WOFF 2.0 — 当前的 Web 字体标准答案

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 的压缩原理

WOFF2 的压缩为什么比 WOFF 强这么多?原因不在某个单点技巧,而是一系列字体领域知识驱动的定制优化的叠加。

1. 字体内部结构认知

字体文件内部由多个独立的"表"(Table)组成:

表名作用占体积比(中文字体)
glyf每个字形的矢量轮廓数据(贝塞尔曲线坐标数组)~70%
loca每个字形在 glyf 表中的偏移索引~5%
cmapUnicode 码点 → 字形编号的映射表~10%
hmtx水平度量信息(字宽、左右留白)~8%
name字体名称、版权等元数据~2%
其他hinting、kerning、GSUB、GPOS 等~5%

WOFF 的做法是先把所有表拼成一个整体,然后用 zlib 压缩整个文件——这种做法简单粗暴,对不同类型的数据一视同仁。

WOFF2 的做法是对每种表用针对性的策略处理

类比理解

WOFF 的做法是把厨房、书房、衣柜的所有物品混在一起装进一个箱子——简单但空间利用率低。

WOFF2 的做法是先把衣服叠好放真空压缩袋(glyf 增量编码)、把书按类别装箱(cmap 范围编码),再把所有处理过的物品放进用高效材料做的箱子(Brotli 整体压缩)。每一步都在用领域知识节约空间。

2. 为什么中文字体压缩效果特别好

中文字体中有大量结构相似的字形。例如"木"和"林"和"森"共享同一个偏旁,"氵"旁的字有数百个。Brotli 的滑动窗口可以捕捉到这些跨字形的重复模式,实现远超通用压缩的效果。这也是为什么 Maple Mono CN 从 18MB → 5.3MB(压缩率 70%)——中文特有的字形冗余反而成为了压缩的"帮手"。

浏览器如何加载字体

字体是浏览器的延迟加载资源——浏览器不会预先下载所有字体,而是等到 CSS 解析到 @font-face 并且实际有文字使用了该字体时才开始下载。这个机制本身是好的(避免下载用不到的字体),但它也带来了字体加载时序上的复杂性。

完整加载流程

Step 1
解析 HTML → 构建 DOM
Step 2
解析 CSS → 发现 @font-face 声明
Step 3
发现使用该字体的 DOM 元素 → 触发下载
Step 4
下载字体文件
Step 5
解析字体 → 替换备用字体渲染

Step 3 是一个关键细节:浏览器只在文本真正使用了某个字重/样式时才下载对应字体。也就是说,如果你的 CSS 声明了 10 种字重,但页面上只用了 Regular (400),理论上浏览器只会下载 Regular 这一个文件。

但在实际排错中发现,浏览器对于大字体的处理更"激进"——当字体文件过大(18MB TTF),即使只使用了一个字重,下载过程本身就会占据网络线程,使得浏览器一直处于"等待资源"状态,标签页持续显示加载中。

font-display 的作用

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 就是基于这个设计决策。

实战:288MB → 84MB 的优化过程

以下是本站字体优化的完整记录,可以作为类似问题的参考流程。

问题表象

❌ 优化前
  • 浏览器标签页持续显示"加载中"转圈
  • Network 面板显示 16 个字体请求,单个体积 15-19MB
  • 字体下载耗时数十秒
  • font-display: swap 已生效(文字可见),但标签页不结束
  • 字体文件总大小:288MB
✅ 优化后
  • 标签页正常完成加载
  • 单个字体文件 4-6MB,下载几秒内完成
  • 文字秒出,字体无感替换
  • 字体文件总大小:84MB

优化步骤

# 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-facesrc 支持多个 url(),可以从 TTF 到 WOFF2 依次降级。但 2026 年 WOFF2 浏览器支持率已达 98%+,提供 WOFF2 单一格式完全够用。多格式兼容反而会增加 CSS 代码量和用户首次访问时可能下载错误格式的概率。

Web 字体最佳实践

1. 优先使用 WOFF2,这是底线

无论什么场景,Web 上发布的字体必须转换为 WOFF2 格式。TTF 是桌面格式,不应该出现在 HTTP 响应中。这个结论适用于任何字体——中文、英文、等宽、衬线,没有例外。

2. 中文字体考虑子集化(Subset)

WOFF2 能把 18MB TTF 压到 5MB,但如果你的网站总共只用了 2,000 个汉字(例如技术博客的常用字集),那么一个包含 20,000+ 字的完整字体是巨大的浪费。

子集化就是从完整字体中提取你实际需要的字符,生成一个只包含这些字符的迷你字体。工具推荐:

对于本站的 Maple Mono CN,目前暂未做子集化,因为笔记仓库是增量更新的,新文章可能引入新汉字,子集维护成本较高。5MB 的 Regular 字重在现代网络条件下加载体验可接受。

子集化类比

完整字体 = 一本《辞海》(所有字都在,但很重)。
子集化字体 = 从《辞海》里把你需要的那几页复印下来装订成册——内容一样,体积只有原来的几十分之一。

3. 限制 @font-face 声明的字重数量

本站声明了 Thin (100) 到 ExtraBold (800) 共 10 个字重,每份 WOFF2 约 5MB。在实际渲染中,大多数页面只会用到 Regular (400)、Bold (700) 两种字重。其他 8 份字体声明虽然不会被下载(浏览器按需加载),但 @font-face 声明本身不消耗带宽——关键不是声明了多少,而是页面上实际用了多少字重

但如果你发现自己确实只用少量字重,就应该精简声明——减少 CSS 中的无用规则,降低维护复杂度。

4. 利用缓存策略

字体文件相比其他静态资源有很大的特殊性:几乎不会改变。一旦字体选定了,可能数月甚至数年不变。因此可以设置比 JS/CSS 更长的缓存时间:

# 字体文件缓存 30 天
location ~* \.(woff|woff2)$ {
    expires 30d;
    add_header Cache-Control "public, immutable";
}

immutable 告诉浏览器"这个文件永不改变",浏览器即使在硬刷新时也不会重新请求该资源。本站当前配置的是 7 天缓存,未来可以适当延长。

5. 使用 preload 优化关键字体

如果 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 的边际收益不大。

6. 决策树速查

格式选择
只提供 WOFF2
加载策略
font-display: swap
体积优化
中文字体考虑子集化
缓存策略
长缓存 + immutable

小结

维度TTFWOFF2
诞生年份19912018
设计目标桌面操作系统本地字体Web 网络传输
压缩算法表级定制处理 + Brotli
典型大小(中文字体单字重)18MB5MB
浏览器支持全部支持(但不该用)98%+(2026)
适合场景本地安装、桌面排版一切 Web 场景

核心认知:

  1. TTF 是桌面格式,不是 Web 格式。就像你不会把 PSD 文件直接嵌在网页里一样,你也不应该把 TTF 直接发布到网站上。
  2. WOFF2 = TTF 的"Web 版"。内部数据完全一致(同一个字体的矢量轮廓、hinting、字距),只是用 Brotli 压缩后体积缩小 60-70%。
  3. 中文字体的体积问题和格式无关(TTF 还是 WOFF2 都是大文件),根本原因是汉字数量多。压缩可以缓解但不能根治——根治靠子集化。
  4. font-display: swap 是文档类网站字体加载的正确策略:先显示、后优化,不让用户等待。配合 WOFF2(小文件)使用体验最佳。
  5. 转换工具很简单brew install woff2woff2_compress font.ttf,一条命令完成。

最后更新:2026 年 6 月 · 本文源于本站上线后的一次字体加载故障排查,记录了从发现 TTF 体积问题到全面迁移 WOFF2 的完整过程。希望这篇笔记能帮你在 Web 字体选择上少走弯路。