引言
你是否遇到过这样的情况:页面加载时,文字先是显示为系统字体,然后突然闪烁变成设计字体?这就是著名的“FOUT”(Flash of Unstyled Text)或“FOIT”(Flash of Invisible Text)。Web字体虽然让页面更美观,但若不加优化,会严重影响用户体验和性能指标。本文将带你深入理解Web字体的加载过程,并提供一套完整的优化方案,包括字体子集化、预加载、font-display策略、缓存优化等,每一步都有可运行的代码示例。
1. 理解字体加载的“罪魁祸首”
当浏览器遇到CSS中的@font-face时,会下载字体文件。在下载完成之前,浏览器有两种行为:
- FOIT:隐藏文字(最多3秒),等待字体加载。
- FOUT:立即使用后备字体,字体加载后替换。
默认情况下,大多数浏览器采用FOIT,这会导致文字不可见,严重影响首次内容绘制(FCP)和最大内容绘制(LCP)。
2. 第一步:使用font-display控制字体渲染策略
font-display是CSS属性,用于控制字体的加载行为。它有五个值:auto、block、swap、fallback、optional。
swap:立即使用后备字体,加载完成后替换(推荐用于品牌字体)。optional:给字体极短时间(约100ms)加载,若超时则使用后备字体,且不替换(适合图标字体)。fallback:类似optional,但替换窗口更短。
代码示例:
@font-face {
font-family: 'MyCustomFont';
src: url('/fonts/myfont.woff2') format('woff2');
font-display: swap; /* 避免FOIT */
font-weight: 400;
font-style: normal;
}
body {
font-family: 'MyCustomFont', 'Helvetica', sans-serif;
}
注意:swap可能导致布局偏移(CLS),因为后备字体和自定义字体的度量不同。可以通过调整size-adjust或使用font-size-adjust来缓解。
3. 第二步:预加载关键字体
使用可以提前下载字体,避免CSS解析后才开始下载。
代码示例:
<link rel="preload" href="/fonts/myfont.woff2" as="font" type="font/woff2" crossorigin>
注意:必须添加crossorigin属性,因为字体请求通常是跨域请求(即使同域)。
最佳实践:只预加载首屏需要的字体,避免过度预加载。
4. 第三步:字体子集化——只加载需要的字符
字体文件通常包含数千个字符,但你的页面可能只用到其中一小部分(例如拉丁字母)。通过子集化,可以大幅减小字体文件大小。
工具:使用glyphhanger或FontTools。
使用glyphhanger示例:
# 安装
global install glyphhanger
# 分析页面中使用的字符
glyphhanger https://example.com --spider
# 生成子集字体(只包含用到的字符)
glyphhanger --whitelist="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" --formats=woff2 --output=./subset-fonts
代码集成:
@font-face {
font-family: 'MySubsetFont';
src: url('/fonts/myfont-subset.woff2') format('woff2');
unicode-range: U+0020-007E; /* 只覆盖ASCII字符 */
font-display: swap;
}
注意:unicode-range告诉浏览器何时使用该字体,避免下载不需要的字体。
5. 第四步:使用WOFF2格式并压缩
WOFF2是当前最佳字体格式,压缩率比WOFF高30%。确保服务器支持WOFF2。
转换工具:使用google/woff2或在线工具。
# 使用woff2_compress
woff2_compress myfont.ttf
服务器配置:添加正确的MIME类型。
# Nginx
location /fonts/ {
add_header Content-Type font/woff2;
}
6. 第五步:利用缓存策略
字体文件通常不常变,应设置强缓存。
HTTP头示例:
Cache-Control: public, max-age=31536000, immutable
Nginx配置:
location ~* \.(woff2|woff)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
7. 第六步:使用字体加载API进行精细控制
CSS Font Loading API允许用JavaScript监听字体加载状态,实现更精细的交互。
代码示例:
const font = new FontFace('MyCustomFont', 'url(/fonts/myfont.woff2)', {
display: 'swap'
});
font.load().then(() => {
document.fonts.add(font);
console.log('Font loaded');
// 可以在此添加类,避免布局偏移
document.body.classList.add('font-loaded');
}).catch(err => {
console.error('Font loading failed', err);
});
8. 完整优化流程示例
假设你有一个自定义字体Poppins,我们将其优化。
步骤1:子集化
使用glyphhanger提取页面用到的字符,生成子集字体。
步骤2:CSS配置
@font-face {
font-family: 'Poppins';
src: url('/fonts/poppins-subset.woff2') format('woff2');
font-display: swap;
font-weight: 400;
font-style: normal;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
body {
font-family: 'Poppins', 'Helvetica Neue', sans-serif;
}
/* 后备字体大小调整,减少CLS */
body {
font-size: 16px;
line-height: 1.5;
}
步骤3:HTML预加载
<link rel="preload" href="/fonts/poppins-subset.woff2" as="font" type="font/woff2" crossorigin>
步骤4:服务器配置
Nginx配置缓存和MIME类型。
9. 常见坑与踩坑经验
- 坑1:预加载未加
crossorigin导致字体被加载两次。 - 坑2:
font-display: swap导致布局偏移,未使用size-adjust或ascent-override等度量调整。 - 坑3:子集化时遗漏了某些字符(如标点符号),导致页面显示空白或乱码。
- 坑4:同时加载多种字体变体(如粗体、斜体),每个都需要单独@font-face。
10. 总结
通过以上步骤,你可以显著提升Web字体的加载性能:
- 使用
font-display: swap避免FOIT - 预加载关键字体
- 子集化减少文件大小
- 使用WOFF2格式
- 设置强缓存
- 利用JavaScript API精细控制
延伸阅读: