字体交付基础:理解字体文件格式、字符集、字重与字库结构
字体交付压缩周期的起点,不是压缩工具的选择,而是对字体文件本身的深度理解。如果团队对 TTF、OTF、WOFF 的差异、字符集覆盖范围、字重命名规则以及字库内部表结构缺乏统一认知,后续的自动化裁剪、子集化、格式转换和缓存策略都将建立在流沙之上。本章将系统拆解字体文件的底层逻辑,帮助你在压缩之前先建立“字体资产体检”的标准动作。
2.1 字体文件格式:TTF、OTF、WOFF 与 WOFF2 的工程差异
核心原理
字体文件格式决定了数据的组织方式、压缩潜力以及浏览器或操作系统的解析路径。TTF(TrueType Font)使用二次贝塞尔曲线描述字形轮廓,OTF(OpenType Font)则允许使用三次贝塞尔曲线(PostScript 轮廓),在复杂字形上通常更紧凑。WOFF(Web Open Font Format)本质上是 TTF/OTF 的容器封装,内部使用 zlib 压缩;WOFF2 则引入 Brotli 压缩和自定义字形重组,平均体积比 WOFF 再减少 30%~50%。
| 格式 | 轮廓技术 | 压缩方式 | 浏览器支持 | 典型体积(中文) | 适用场景 |
|---|---|---|---|---|---|
| TTF | TrueType(二次曲线) | 无 | 全平台 | 8~20MB | 桌面安装、兼容旧系统 |
| OTF | PostScript(三次曲线) | 无 | 全平台 | 7~18MB | 设计工具、印刷 |
| WOFF | 继承 TTF/OTF | zlib | IE9+、现代浏览器 | 4~10MB | Web 字体基础交付 |
| WOFF2 | 继承 TTF/OTF | Brotli + 字形重组 | Chrome 36+、Safari 10+ | 2~6MB | Web 字体首选 |
判断方法
在交付压缩前,先通过 fonttools 或 ttx 工具检查字体头部信息,确认 sfnt 版本标识:0x00010000 为 TTF,OTTO 为 OTF,wOFF 为 WOFF,wOF2 为 WOFF2。同时检查 glyf 表是否存在——若存在则为 TrueType 轮廓,若存在 CFF 表则为 PostScript 轮廓。
# 使用 fonttools 快速识别格式
from fontTools.ttLib import TTFont
font = TTFont("example.woff2")
print(font.sfntVersion) # 输出 'wOF2'
print("glyf" in font, "CFF " in font) # 判断轮廓类型
案例拆解
某电商平台在首屏字体加载中,原先直接交付 TTF 格式(12MB),导致 LCP 高达 4.2s。团队将字体转换为 WOFF2 后体积降至 5.1MB,但发现 iOS 10 以下设备无法解析。最终采用 WOFF2 为主、WOFF 为降级方案,通过 @font-face 的 format 提示让浏览器自动选择,LCP 降至 2.1s。
@font-face 中同时声明 WOFF 作为回退,且 WOFF 应放在 WOFF2 之后。
常见误区
- 误以为 OTF 一定比 TTF 小——实际取决于字形复杂度,CFF 轮廓在拉丁字体中更优,但中文差异不大。
- 忽略 WOFF2 的 Brotli 压缩对 CPU 的解压开销——在低端移动设备上,解压时间可能抵消体积优势。
- 直接使用在线转换工具而不检查表结构完整性,导致部分字形渲染异常。
2.2 字符集与 cmap 表:理解字体覆盖范围
核心原理
字符集定义了字体能渲染哪些字符。Unicode 是国际标准编码,GB2312/GBK 是中文国家标准,Big5 是繁体中文常用编码。字体内部的 cmap 表负责将 Unicode 码点映射到字形索引(glyph ID)。交付压缩的核心手段之一就是子集化——只保留页面实际用到的字符,从而大幅削减体积。
判断方法
使用 fontTools.subset 模块或 pyftsubset 命令行工具,按 Unicode 范围或文本文件进行裁剪。先统计页面实际字符覆盖,再决定保留哪些区块:基本拉丁(U+0000-007F)、常用中文(U+4E00-9FFF)、标点符号(U+3000-303F)等。
# 按文本文件子集化
pyftsubset source.woff2 --text-file=page_chars.txt --output-file=subset.woff2
# 按 Unicode 范围子集化
pyftsubset source.woff2 --unicodes=U+0000-00FF,U+4E00-9FFF --output-file=subset.woff2
案例拆解
某新闻客户端需要支持文章正文、标题、评论三种场景。初始方案加载完整字体(15MB),后改为按页面动态子集化:正文保留 3500 常用字,标题保留 500 字,评论保留 200 字。通过服务端渲染时动态生成子集字体,首屏字体体积从 15MB 降至 180KB,加载时间减少 92%。但代价是每次新文章发布需重新生成子集,且 CDN 缓存命中率下降。
| 场景 | 字符数 | 子集体积 | 缓存策略 | 回退方案 |
|---|---|---|---|---|
| 正文 | 3500 | ~120KB | 按文章 ID 缓存 | 系统字体 |
| 标题 | 500 | ~40KB | 按模板缓存 | 加粗系统字体 |
| 评论 | 200 | ~20KB | 全局缓存 | 忽略特殊字符 |
常见误区
- 只按 Unicode 范围裁剪,未考虑页面动态插入的用户生成内容(UGC),导致生僻字显示为豆腐块。
- 忽略标点符号和数字的覆盖,中文引号、破折号等特殊字符缺失。
- 子集化后未保留
cmap表的格式 12(支持 Unicode 全量),导致部分码点映射丢失。
2.3 字重体系:从 Thin 到 Black 的命名与映射
核心原理
字重(Weight)描述字形的粗细程度。CSS 规范定义了 100~900 的九个等级,但字体文件内部通过 OS/2 表的 usWeightClass 字段记录数值。常见命名映射:Thin=100、Light=300、Regular=400、Medium=500、Bold=700、Black=900。交付压缩时,字重数量直接影响 HTTP 请求数——每增加一个字重,就多一个字体文件请求。
判断方法
检查字体家族中实际包含的字重文件,使用 fonttools 读取 OS/2 表:
from fontTools.ttLib import TTFont
font = TTFont("Inter-Bold.woff2")
os2 = font["OS/2"]
print(os2.usWeightClass) # 输出 700
案例拆解
某设计系统最初引入 6 个字重(Light/Regular/Medium/SemiBold/Bold/Black),导致字体请求总重达 3.2MB。通过分析页面实际使用,发现 SemiBold 和 Black 使用率不足 0.5%。团队决定删除这两个字重,改用 font-synthesis 或 CSS font-weight 的合成加粗替代,请求数从 6 降至 4,总重降至 2.1MB。代价是部分设计稿中的 SemiBold 视觉效果略有偏差,但通过调整字间距和颜色补偿。
常见误区
- 将字重名称与数值混淆,例如把 Medium 误认为 600(实际为 500)。
- 忽略可变字体中字重轴(wght)的映射范围,导致 CSS 中设置的数值超出字体支持范围。
- 在
@font-face中未正确声明font-weight和font-style,导致浏览器加载错误字重。
2.4 字库结构:核心表与子集化裁剪空间
核心原理
字体文件内部由多个表(Table)组成,每个表承担特定功能。关键表包括:glyf(字形轮廓)、loca(字形索引偏移)、cmap(字符映射)、head(全局信息)、OS/2(兼容性指标)、hhea(水平度量)、kern(字距调整)。子集化工具会重建这些表,只保留被选中的字形及其度量数据。
| 表名 | 功能 | 子集化影响 | 压缩关注点 |
|---|---|---|---|
| glyf | 存储字形轮廓点 | 体积最大,裁剪收益最高 | 检查是否包含重复轮廓 |
| loca | 记录每个字形的偏移 | 随 glyf 自动重建 | 短格式 vs 长格式 |
| cmap | Unicode 到字形 ID 映射 | 保留所需子集 | 格式 4/12 的选择 |
| OS/2 | 字重、度量、兼容性 | 保留但需更新 | usWeightClass 正确性 |
| kern | 字距调整 | 可能被裁剪 | 中文场景影响较小 |
判断方法
使用 ttx 工具将字体转储为 XML,检查表列表和每个表的实际内容。重点关注 glyf 表中是否包含空字形、重复轮廓,以及 cmap 表的子表格式是否覆盖所需语言。
# 转储字体表结构
ttx -l source.woff2 # 列出所有表
ttx -t cmap -t OS/2 source.woff2 # 导出指定表
案例拆解
某在线文档产品需要支持中日韩(CJK)字符,原始字体包含 65,000+ 字形,体积 22MB。通过分析发现,实际文档中仅使用约 8,000 个汉字和 2,000 个日文假名。团队使用 pyftsubset 按文本统计结果裁剪,同时移除 kern 表和部分 GPOS 特性,最终体积降至 1.8MB。但发现日文假名的拗音(小写假名)缺失,原因是文本统计时未包含所有 Unicode 码点,后通过补充字符集解决。
kern 表可能导致拉丁字符间距异常,尤其在标题或 Logo 场景。建议保留 kern 表或使用 GPOS 的 kerning 特性替代。
常见误区
- 认为子集化后字体一定更小——如果页面字符覆盖超过 80%,子集化收益有限,反而增加服务端计算开销。
- 忽略
head表中的unitsPerEm值,导致子集化后度量异常。 - 未检查
post表中的字形名称,部分工具依赖名称索引,裁剪后可能报错。
2.5 本章总结与交付检查清单
字体交付压缩的起点是“理解文件内部结构”,而非盲目压缩。在进入后续章节的自动化流水线之前,请确保团队对以下问题有统一答案:当前字体是什么格式?覆盖哪些字符集?包含几个字重?哪些表可以被安全裁剪?
- 确认字体格式为 WOFF2(主)+ WOFF(降级),并验证浏览器兼容性。
- 统计页面实际字符覆盖,生成子集化字符集文件。
- 检查字重数量,删除使用率低于 1% 的字重或改用可变字体。
- 使用
fonttools验证子集化后的表结构完整性。 - 在真实网络环境下测试加载性能,对比压缩前后的 LCP 和 FCP。