自定义Web字体能让页面视觉表现更上一层楼,但一个中文字体动辄几MB,如果直接从源站服务器输出,用户在浏览器中经常遇到文字长时间不显示或者字体闪烁的问题。WOFF2格式的出现让字体体积压缩到了可接受的范围,而多云CDN则解决了全球范围内不同地域用户访问延迟不一致的难题。本文将围绕WOFF2字体优化与多云CDN加速这两条主线,完整梳理一套可落地的字体性能优化方案。

一、为什么选择WOFF2:字体格式选型与原理
Web字体的常见格式有TTF、OTF、EOT、WOFF和WOFF2几种。EOT是IE时代的产物,基本可以淘汰;TTF和OTF是原始字体格式,没有任何针对Web的压缩;WOFF在TTF基础上采用了zlib压缩,体积约为原始格式的60%左右;而WOFF2使用Brotli压缩算法,并引入了transform技术对字体内部结构做预处理,压缩率比WOFF再提升约30%,通常只有原始TTF体积的30%到40%。
除了压缩率,WOFF2还支持字体分片存储的标准。一个WOFF2文件内部可以按字符范围拆分成多个数据块,浏览器加载时可以只下载页面实际用到的字符块。这一点对中文场景极其重要,因为中文字符数量庞大,按需加载意味着首屏可能只需要下载几十KB而不是几MB。
浏览器兼容性方面,WOFF2从Chrome 36、Firefox 39、Safari 10、Edge 14开始支持,覆盖了当前绝大多数活跃浏览器。稳妥的做法是在CSS的font-face中同时声明WOFF2和WOFF回退,浏览器会按照声明顺序优先选择WOFF2:
@font-face {
font-family: 'MyFont';
src: url('https://cdn.example-font-cdn.com/fonts/myfont.woff2') format('woff2'),
url('https://cdn.example-font-cdn.com/fonts/myfont.woff') format('woff');
font-weight: 400;
font-style: normal;
font-display: swap;
}
把WOFF2放在src声明的最前面,不支持WOFF2的老浏览器会自动跳过并尝试WOFF,这样既享受了新格式的体积优势,又不会丢失兼容性。
二、字体本地优化:子集化与按需拆分
接入CDN之前,先把字体本身的体积压下来,这是收益最大的一步。对英文字体来说,直接用工具转成WOFF2即可,通常从200KB压到50KB以内。但中文字体不同,完整的中文字库包含两万多个字符,即使WOFF2压缩后往往还有3MB到8MB,必须做子集化处理。
子集化有两种思路。第一种是基于固定字符集,比如只保留常用3500字加页面实际出现的字符,可以使用fonttools或者glyphhanger这类工具完成:
# 安装fonttools并使用pyftsubset做子集化 pip install fonttools brotli pyftsubset SourceHanSans-Regular.ttf \ --text-file=chars.txt \ --flavor=woff2 \ --output-file=subset.woff2 # 或者使用glyphhanger自动抓取页面用到的字符 npx glyphhanger https://www.ipipp.com \ --subset=SourceHanSans-*.ttf \ --formats=woff2
第二种思路是按unicode-range拆分成多个分片,把整个字库切成几十上百个小文件,每个文件负责一段Unicode区间,配合CSS的unicode-range属性让浏览器按需拉取。这种方案不需要维护字符集列表,特别适合内容动态变化的站点。一个分片的CSS大概长这样:
/* 分片1:基本拉丁字母区 */
@font-face {
font-family: 'HanSans';
src: url('https://cdn.example-font-cdn.com/fonts/hansans-001.woff2') format('woff2');
unicode-range: U+0000-00FF;
font-display: swap;
}
/* 分片2:CJK常用汉字区 */
@font-face {
font-family: 'HanSans';
src: url('https://cdn.example-font-cdn.com/fonts/hansans-002.woff2') format('woff2');
unicode-range: U+4E00-9FA5;
font-display: swap;
}
需要强调font-display属性的取值。swap表示先用系统字体渲染,字体加载完成后再切换,用户不会看到白屏但会有一次字体闪动;optional在超过100ms未加载完成时直接放弃本次自定义字体,后续导航才使用,体验最平滑;block则会阻塞文字显示最多3秒。对内容型站点,推荐swap或optional,避免block带来的长时间不可见文字。
三、多云CDN加速:架构设计与缓存策略
本地优化做完后,字体文件的传输就要交给CDN了。所谓多云CDN,是指同时接入两家或以上的CDN服务商,通过智能调度把用户请求路由到当前时刻表现最好的节点。单一CDN在个别区域或者高峰期可能出现节点回源慢、命中率下降的问题,多云方案可以互相兜底,同时还能在一家服务商故障时快速切换,可用性明显更高。
多云架构通常有三种实现方式。第一种是DNS层调度,通过自建或者使用第三方智能DNS服务,根据用户地理位置和各CDN健康状态返回不同的CNAME;第二种是客户端调度,在页面中嵌入一段探测脚本,测速后动态决定字体URL指向哪个云;第三种是云厂商提供的多CDN管理平台,直接托管调度逻辑。中小团队推荐第一种,实现简单且不依赖前端改动:
用户请求 fonts.yourdomain.com
|
智能 DNS 解析
|
+---------+---------+
| |
CDN-A 节点 CDN-B 节点
| |
+----> 共享对象存储源站(存放WOFF2分片文件)
缓存配置上有几个关键点。第一,字体文件是典型的静态不可变资源,文件名中带上内容哈希,比如hansans-002.a3f9c1.woff2,然后设置超长缓存时间:
Cache-Control: public, max-age=31536000, immutable Access-Control-Allow-Origin: *
第二,字体从CDN域名加载属于跨域请求,必须在CDN或者源站返回Access-Control-Allow-Origin响应头,同时CSS中的font-face声明建议加上crossorigin属性,否则浏览器会重复下载字体,因为匿名请求和凭据请求的缓存是分开的:
<link rel="preload" href="https://cdn.example-font-cdn.com/fonts/hansans-001.woff2"
as="font" type="font/woff2" crossorigin>
第三,对首屏关键字体使用preload预加载,可以让字体请求提前于CSS解析发起,减少几百毫秒的等待。但preload只能用于确定会被用到的分片,滥用反而会抢占带宽,一般预加载一到两个文件即可。
四、落地实践与常见踩坑
完整落地时建议的流程是:设计稿定稿后,从字体文件生成WOFF2子集分片,上传到对象存储,刷新两家CDN的缓存预热,然后在构建阶段自动生成带哈希的font-face CSS,最后配置监控告警观察命中率与加载耗时。可以用下面的脚本在CI中自动完成分片与上传:
#!/bin/bash
# 构建阶段:切分字体并上传到对象存储
set -e
python split_font.py --input SourceHanSans.ttf --out-dir ./dist/fonts
# 上传并设置缓存头
for f in ./dist/fonts/*.woff2; do
ossutil cp "$f" oss://font-bucket/fonts/ \
--meta Cache-Control:public,max-age=31536000,immutable
done
# 刷新两家CDN缓存
cdn-a-cli purge --path https://cdn-a.example-font-cdn.com/fonts/
cdn-b-cli purge --path https://cdn-b.example-font-cdn.com/fonts/
实际项目中容易踩的坑有几个。一是子集字符集遗漏,页面动态渲染的新字符不在子集范围内会直接回退到系统字体,造成混排,建议在CI中加入字符覆盖率检查;二是忘了配置CORS导致字体加载报错,控制台会出现字体被同源策略拦截的提示;三是DNS调度切换云厂商时缓存key不一致造成命中率波动,最好保证两个云回源到同一个对象存储地址;四是把preload加到了所有分片上,反而拖慢了首屏关键资源,preload应该精打细算。
验证优化效果可以用Chrome DevTools的Network面板过滤font请求,观察字体文件的大小、命中缓存的情况和加载时机,也可以用Lighthouse跑一次性能审计,关注字体加载对LCP的影响。经过WOFF2加子集化加多云CDN的组合优化后,中文字体首屏加载量从几MB降到一两百KB是完全可以实现的目标,用户几乎感知不到字体切换的闪动。
总结来说,字体优化的正确顺序是先减体积、再拆分片、最后上多云CDN做分发和容灾,三步走完,字体加载就不再是页面性能的短板了。