字体交付基础:理解字体文件格式、字符集、字重与字库结构

字体交付压缩周期的起点,不是压缩工具的选择,而是对字体文件本身的深度理解。如果团队对 TTF、OTF、WOFF 的差异、字符集覆盖范围、字重命名规则以及字库内部表结构缺乏统一认知,后续的自动化裁剪、子集化、格式转换和缓存策略都将建立在流沙之上。本章将系统拆解字体文件的底层逻辑,帮助你在压缩之前先建立“字体资产体检”的标准动作。

字体文件核心概念关系图 字体文件(Font File) 文件格式 TTF / OTF / WOFF / WOFF2 EOT / SVG(旧) → 决定压缩率与兼容性 字符集(Charset) Unicode / GB2312 / Big5 cmap 表映射 → 决定覆盖范围与体积 字重(Weight) Thin → Black(100-900) OS/2 表 usWeightClass → 决定请求数量与加载策略 字库结构(Tables) glyf / loca / cmap / head OS/2 / hhea / kern → 决定子集化与裁剪空间 交付压缩 = 格式选择 × 字符集裁剪 × 字重合并 × 表结构优化

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%。

格式轮廓技术压缩方式浏览器支持典型体积(中文)适用场景
TTFTrueType(二次曲线)无全平台8~20MB桌面安装、兼容旧系统
OTFPostScript(三次曲线)无全平台7~18MB设计工具、印刷
WOFF继承 TTF/OTFzlibIE9+、现代浏览器4~10MBWeb 字体基础交付
WOFF2继承 TTF/OTFBrotli + 字形重组Chrome 36+、Safari 10+2~6MBWeb 字体首选

判断方法

在交付压缩前,先通过 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。

风险提示:WOFF2 虽然压缩率高,但部分老旧 Android WebView 不支持。务必在 @font-face 中同时声明 WOFF 作为回退,且 WOFF 应放在 WOFF2 之后。

常见误区

  • 误以为 OTF 一定比 TTF 小——实际取决于字形复杂度,CFF 轮廓在拉丁字体中更优,但中文差异不大。
  • 忽略 WOFF2 的 Brotli 压缩对 CPU 的解压开销——在低端移动设备上,解压时间可能抵消体积优势。
  • 直接使用在线转换工具而不检查表结构完整性,导致部分字形渲染异常。

2.2 字符集与 cmap 表:理解字体覆盖范围

核心原理

字符集定义了字体能渲染哪些字符。Unicode 是国际标准编码,GB2312/GBK 是中文国家标准,Big5 是繁体中文常用编码。字体内部的 cmap 表负责将 Unicode 码点映射到字形索引(glyph ID)。交付压缩的核心手段之一就是子集化——只保留页面实际用到的字符,从而大幅削减体积。

关键指标:一个完整的中文字体(GB2312 约 6763 字)TTF 体积约 8~12MB,而仅包含 100 个常用汉字的子集字体可压缩至 50~100KB,压缩率超过 99%。

判断方法

使用 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 视觉效果略有偏差,但通过调整字间距和颜色补偿。

经验:在压缩交付中,优先保留 Regular 和 Bold 两个字重,其余字重按需加载或使用可变字体(Variable Font)技术合并为一个文件。

常见误区

  • 将字重名称与数值混淆,例如把 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 长格式
cmapUnicode 到字形 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 本章总结与交付检查清单

字体交付压缩的起点是“理解文件内部结构”,而非盲目压缩。在进入后续章节的自动化流水线之前,请确保团队对以下问题有统一答案:当前字体是什么格式?覆盖哪些字符集?包含几个字重?哪些表可以被安全裁剪?

交付前自检清单:
  1. 确认字体格式为 WOFF2(主)+ WOFF(降级),并验证浏览器兼容性。
  2. 统计页面实际字符覆盖,生成子集化字符集文件。
  3. 检查字重数量,删除使用率低于 1% 的字重或改用可变字体。
  4. 使用 fonttools 验证子集化后的表结构完整性。
  5. 在真实网络环境下测试加载性能,对比压缩前后的 LCP 和 FCP。