为什么你的网站还在被字体拖慢?
在 Web 性能优化中,字体往往是最容易被忽视的一环。一个 300KB 的中文字体文件,可能让首屏渲染延迟 1 秒以上;一个未优化的 @font-face 声明,可能导致文字长时间不可见(FOIT)或突然跳动(FOUT)。更隐蔽的是,字体切换引发的布局偏移(CLS)会直接拉低 Core Web Vitals 评分。
本文将从实际场景出发,带你一步步完成字体性能优化。读完你将掌握:
- 如何精准分析字体加载瓶颈
- 字体子集化与格式选择的最佳实践
- 预加载、
font-display与 CSS 度量覆盖的组合拳 - 动态字体的按需加载策略
1. 诊断:你的字体到底慢在哪里?
优化之前,先测量。打开 Chrome DevTools 的 Network 面板,筛选 Font 类型,观察:
- 字体文件大小(通常中文字体 2-10MB,英文字体 20-100KB)
- 加载时间与 TTFB
- 是否阻塞了文本渲染
更专业的方式是使用 Lighthouse 的“避免巨大的网络负载”和“确保文本在 Web 字体加载期间保持可见”审计项。
关键指标:
- FOIT(Flash of Invisible Text):字体加载期间文本不可见,通常由
font-display: block引起。 - FOUT(Flash of Unstyled Text):先用系统字体显示,字体加载后替换,可能导致布局偏移。
- CLS(Cumulative Layout Shift):字体替换时字符宽度变化导致的布局移动。
2. 字体子集化:只加载用到的字符
中文字体动辄数 MB,但页面可能只用了 500 个汉字。子集化是最高效的优化手段。
2.1 使用 fontmin 进行子集化
安装:
npm install fontmin --save-dev
创建一个 Node.js 脚本 subset.js:
const Fontmin = require('fontmin');
const fs = require('fs');
// 提取页面中用到的所有字符
const text = fs.readFileSync('./src/index.html', 'utf8');
const usedChars = Array.from(new Set(text.replace(/\s/g, ''))).join('');
const fontmin = new Fontmin()
.src('./fonts/SourceHanSans-Regular.ttf')
.use(Fontmin.glyph({
text: usedChars, // 只保留这些字符
hinting: false // 移除 hinting 减小体积
}))
.use(Fontmin.ttf2woff2()) // 转换为 woff2 格式
.dest('./dist/fonts/');
fontmin.run((err, files) => {
if (err) throw err;
console.log('子集化完成,生成文件:', files.map(f => f.path));
});
运行 node subset.js,你会得到一个只有几十 KB 的 woff2 文件。
💡 注意:如果页面内容是动态的(如用户评论),子集化可能遗漏字符。此时可以保留一个完整的后备字体,或使用 unicode-range 分片加载。
2.2 使用 unicode-range 按需分片
对于大型多语言站点,可以将字体按 Unicode 范围拆分为多个文件:
@font-face {
font-family: 'MyFont';
src: url('myfont-latin.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153;
font-display: swap;
}
@font-face {
font-family: 'MyFont';
src: url('myfont-chinese.woff2') format('woff2');
unicode-range: U+4E00-9FFF;
font-display: swap;
}
浏览器只会下载当前页面实际用到的字符所在的分片,极大减少流量。
3. 预加载关键字体
字体通常是在 CSS 解析后才发现并请求的,这会导致延迟。使用 提前触发下载:
<link rel="preload" href="/fonts/myfont-subset.woff2" as="font" type="font/woff2" crossorigin>
重要:crossorigin 属性必须加,即使字体同源。否则浏览器会发起两次请求。
💡 注意:只预加载首屏必需的字体,避免与关键 CSS/JS 争抢带宽。
4. 选择合适的 font-display 策略
font-display 决定字体加载期间文本的显示方式:
| 值 | 行为 | 适用场景 | |—-|——|———-| | auto | 浏览器默认(通常类似 block) | 不推荐 | | block | 短暂不可见(约 3s),然后回退 | 图标字体 | | swap | 立即用回退字体显示,加载后替换 | 正文文本 | | fallback | 极短不可见(约 100ms),然后回退 | 平衡型 | | optional | 极短不可见,若未加载则使用回退且不替换 | 性能优先 |
推荐:正文使用 swap,标题使用 optional 或 fallback。
@font-face {
font-family: 'BodyFont';
src: url('body.woff2') format('woff2');
font-display: swap;
}
5. 消除 CLS:使用 CSS 度量覆盖
即使使用 swap,字体替换时字符宽度不同仍会导致布局偏移。解决方案是让回退字体的度量与自定义字体接近。
5.1 使用 size-adjust 和 ascent-override
@font-face {
font-family: 'BodyFont';
src: url('body.woff2') format('woff2');
font-display: swap;
size-adjust: 105%; /* 调整整体大小 */
ascent-override: 90%; /* 调整上伸部 */
descent-override: 22%; /* 调整下伸部 */
line-gap-override: 0%;
}
/* 回退字体定义 */
@font-face {
font-family: 'BodyFont-Fallback';
src: local('Arial');
size-adjust: 105%;
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
}
body {
font-family: 'BodyFont', 'BodyFont-Fallback', sans-serif;
}
5.2 自动生成度量覆盖
手动计算很麻烦,可以使用 fontaine 或 @capsizecss/metrics 自动生成。
安装:
npm install @capsizecss/metrics
示例脚本:
const { createFontStack } = require('@capsizecss/core');
const { Roboto } = require('@capsizecss/metrics/roboto');
const { Arial } = require('@capsizecss/metrics/arial');
const { fontFamily, fontFaces } = createFontStack([Roboto, Arial]);
console.log(fontFamily); // 生成的 font-family 值
console.log(fontFaces); // 对应的 @font-face 规则
将生成的 CSS 插入样式表即可。
6. 动态字体按需加载(进阶)
对于用户生成内容或富文本编辑器,可以监听字体加载事件,动态调整:
document.fonts.ready.then(() => {
console.log('所有字体加载完成');
document.body.classList.add('fonts-loaded');
});
// 或者监听特定字体
document.fonts.load('1em BodyFont').then(() => {
console.log('BodyFont 已加载');
});
配合 CSS:
body {
font-family: 'BodyFont-Fallback', sans-serif;
}
.fonts-loaded body {
font-family: 'BodyFont', 'BodyFont-Fallback', sans-serif;
}
7. 完整示例:优化一个中文博客的字体
假设我们有一个中文博客,使用思源黑体。
步骤 1:提取页面用到的字符,用 fontmin 子集化,得到 sourcehan-subset.woff2(约 80KB)。
步骤 2:HTML 中预加载:
<link rel="preload" href="/fonts/sourcehan-subset.woff2" as="font" type="font/woff2" crossorigin>
步骤 3:CSS 定义:
@font-face {
font-family: 'SourceHan';
src: url('/fonts/sourcehan-subset.woff2') format('woff2');
font-display: swap;
size-adjust: 100%;
ascent-override: 88%;
descent-override: 12%;
line-gap-override: 0%;
}
@font-face {
font-family: 'SourceHan-Fallback';
src: local('PingFang SC'), local('Microsoft YaHei');
size-adjust: 100%;
ascent-override: 88%;
descent-override: 12%;
line-gap-override: 0%;
}
body {
font-family: 'SourceHan', 'SourceHan-Fallback', sans-serif;
}
步骤 4:使用 Lighthouse 验证 CLS 从 0.25 降至 0.02。
8. 常见坑与最佳实践
- 坑 1:忘记
crossorigin导致重复下载。 - 坑 2:
font-display: block导致 FOIT 时间过长,用户看到空白。 - 坑 3:子集化时未包含标点符号,导致显示为方框。
- 坑 4:使用
local()时,不同系统字体差异大,需测试多平台。 - 最佳实践:始终提供系统字体回退栈,并确保回退字体与自定义字体度量接近。
- 最佳实践:使用
font-display: swap+ 预加载 + 度量覆盖,三者结合效果最佳。
总结
Web 字体优化不是单一技巧,而是一套组合策略:子集化减小体积 → 预加载提前请求 → font-display 控制渲染 → 度量覆盖消除 CLS。每一步都能带来可量化的性能提升。
建议你从今天开始,用 Lighthouse 审计自己的网站,按照本文步骤逐一优化。字体虽小,却直接影响用户体验和 SEO 排名。
延伸阅读: