3、字体加载策略:@font-face 声明、font-display 属性、预加载与懒加载
字体加载,说白了就是「怎么让浏览器拿到字体文件」。这事看着简单,但坑特别多。我早年做过一个内容型网站,字体文件快 300KB,结果首屏白屏了快两秒——用户啥都看不见,就盯着空白页面发呆。
后来我才意识到,字体加载策略不是「能不能用」的问题,而是「什么时候用、怎么用、用不好会怎样」的问题。这一章,咱们就把 @font-face 声明、font-display 属性、预加载和懒加载这几个核心手段掰开揉碎讲清楚。
核心观点:字体加载策略的目标不是「让字体最快出现」,而是「让文字最快可见」。有时候,用系统字体先顶着,比死等自定义字体要明智得多。
3.1 @font-face 声明:别写错了,坑很多
@font-face 是字体加载的起点。你写对了,浏览器才知道去哪下载字体、用什么格式、怎么匹配。写错了?字体直接不生效,而且控制台还不报错——我当年就被这个坑过,排查了半天。
一个标准的 @font-face 声明长这样:
@font-face {
font-family: 'MyFont';
src: url('/fonts/myfont.woff2') format('woff2'),
url('/fonts/myfont.woff') format('woff');
font-weight: 400;
font-style: normal;
font-display: swap;
}
这里有几个关键点,我一个个说。
3.1.1 src 顺序决定加载行为
浏览器会按 src 列表的顺序依次尝试。一旦某个格式能解析,后面的就不管了。所以,优先放 woff2,因为它压缩率最高、体积最小。woff 作为降级方案放第二位。别把 ttf 或 eot 放前面,除非你还在兼容 IE8——不过现在应该没人这么干了吧?
3.1.2 font-weight 和 font-style 必须写对
很多人只写一个 @font-face,然后通过 CSS 的 font-weight: bold 去控制粗细。结果呢?浏览器找不到对应的粗体文件,就自己「模拟加粗」——那个效果,惨不忍睹。
正确的做法是:每个字重单独写一个 @font-face:
@font-face {
font-family: 'MyFont';
src: url('/fonts/myfont-regular.woff2') format('woff2');
font-weight: 400;
}
@font-face {
font-family: 'MyFont';
src: url('/fonts/myfont-bold.woff2') format('woff2');
font-weight: 700;
}
我的习惯:我会在项目初期就把所有字重的 @font-face 声明写在一个单独的 _fonts.scss 或 fonts.css 文件里。这样后续维护时,改一个地方就行,不会漏掉。
3.1.3 unicode-range:只加载你需要的字符
中文字体文件动不动就几 MB,因为包含了几万个汉字。但你的页面真的需要所有汉字吗?
比如一个英文为主的网站,可能只需要 26 个字母加数字。用 unicode-range 可以只加载这些字符的子集:
@font-face {
font-family: 'MyFont';
src: url('/fonts/myfont-latin.woff2') format('woff2');
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;
}
这样浏览器只会下载包含这些字符的字体文件。对于中文场景,你可以用工具(比如 fonttools 或 glyphhanger)把字体按常用字、次常用字拆成多个子集。我做过一个项目,把 5MB 的中文字体拆成 3 个子集后,首屏字体体积降到了 800KB——效果立竿见影。
3.2 font-display 属性:控制「等待还是妥协」
font-display 是 CSS 里一个很小的属性,但它决定了用户在字体加载期间的体验。说白了,就是告诉浏览器:你是愿意让用户多等一会儿,还是先用系统字体顶着?
它有五个可选值,我整理了一张表:
| 值 | 阻塞期 | 交换期 | 失败期 | 适用场景 |
|---|---|---|---|---|
auto |
取决于浏览器(通常很短) | 无限 | 使用后备字体 | 默认值,不推荐手动设置 |
block |
3秒 | 无限 | 使用后备字体 | 品牌字体、Logo 等必须展示的场景 |
swap |
极短(约 0.1 秒) | 无限 | 使用后备字体 | 大多数正文场景,推荐使用 |
fallback |
约 0.1 秒 | 约 3 秒 | 使用后备字体 | 希望字体加载但不希望布局抖动 |
optional |
极短(约 0.1 秒) | 无 | 使用后备字体 | 网络差时完全放弃自定义字体 |
这里我重点说两个最常用的值。
3.2.1 swap:最稳妥的选择
swap 的逻辑是:先用系统字体显示文字,等自定义字体加载完成后,再「交换」过去。用户不会看到空白文字,但会看到一次「字体闪烁」(FOUT, Flash of Unstyled Text)。
我个人习惯在正文场景里都用 swap。为什么呢?因为用户能第一时间看到内容,体验上比白屏好太多了。那个字体闪烁,其实大多数用户根本注意不到。
3.2.2 block:慎用,除非你确定字体很快
block 会让浏览器在最多 3 秒内「隐藏文字」,死等字体加载。3 秒后如果还没加载完,才用后备字体显示。
这个值适合什么场景?Logo、品牌标题这种「不用这个字体就没意义」的元素。但你要确保字体文件足够小、CDN 足够快。我曾经在一个移动端项目里对标题用了 block,结果用户在 3G 网络下等了快 4 秒才看到标题——那体验,简直灾难。
避坑指南:不要在正文大段文字上使用 block。3 秒的阻塞期足够让用户关掉页面了。如果你不确定网络状况,优先用 swap 或 fallback。
3.3 预加载:让关键字体提前下载
默认情况下,浏览器只有在解析到使用该字体的 CSS 规则时,才会去下载字体文件。这意味着,如果字体声明在 CSS 文件里,而 CSS 文件又在页面底部,那字体加载就会延迟。
预加载(preload)可以打破这个顺序。你可以在 HTML 的 <head> 里告诉浏览器:这个字体很重要,先下载它。
<link rel="preload" href="/fonts/myfont-bold.woff2" as="font" type="font/woff2" crossorigin>
注意几个细节:
as="font"必须写,否则浏览器不会优先处理type="font/woff2"帮助浏览器判断是否支持该格式crossorigin必须加,即使字体和页面同源。这是浏览器的安全策略,不加的话字体可能被忽略
那什么时候该用预加载?我总结了两条原则:
- 首屏关键字体:比如 Hero 区域的标题字体、Logo 字体
- 字体文件较小:一般小于 50KB 的字体,预加载的收益最大
但别滥用。预加载会占用浏览器的网络带宽和连接数。如果你预加载了 5 个字体文件,那其他资源(图片、JS)就得排队等着。我见过一个项目,预加载了 8 个字体,结果首屏加载时间反而变长了——因为字体把带宽占满了。
我的做法:只预加载 1-2 个最关键的字重(通常是 regular 和 bold),其他的交给 @font-face 自己去发现。你想想看,用户滚动到第二屏时,字体才加载,其实完全来得及。
3.4 懒加载:非关键字体按需加载
懒加载的思路正好和预加载相反:不着急用的字体,就别急着下载。
什么场景适合懒加载?
- 弹窗、浮层里用到的装饰性字体
- 页面底部、用户需要滚动才能看到的内容
- 多语言网站中,非默认语言的字体
实现方式很简单,用 JavaScript 动态创建 <link> 或通过 CSS 的 @font-face 在特定时机触发加载:
// 当用户滚动到某个区域时,动态加载字体
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = '/fonts/decorative-font.css';
document.head.appendChild(link);
observer.unobserve(entry.target);
}
});
});
observer.observe(document.querySelector('.decorative-section'));
还有一种更轻量的方式:利用 CSS 的 @font-face 本身是「按需解析」的特性。只要你不把字体用在任何 DOM 元素上,浏览器就不会下载它。所以,你可以把非关键字体的 @font-face 声明放在一个单独的 CSS 文件里,等需要时再加载这个 CSS 文件。
经验之谈:我曾经在一个多语言电商网站里,把日文字体和韩文字体做成了懒加载。默认只加载中文字体,当用户切换到日语或韩语界面时,才动态加载对应的字体文件。首屏字体体积从 3.2MB 降到了 1.1MB,加载时间减少了 40%。
3.5 组合策略:什么时候用什么
实际项目中,很少只用一种策略。我一般会这样组合:
| 字体类型 | 加载策略 | font-display | 说明 |
|---|---|---|---|
| Logo / 品牌字体 | 预加载 | block 或 swap | 体积小、视觉重要,优先加载 |
| 正文 Regular | @font-face 自动发现 | swap | 最常用,保证文字可见性 |
| 正文 Bold | 预加载(如果首屏需要) | swap | 标题常用,可考虑预加载 |
| 装饰性字体 | 懒加载 | swap 或 optional | 非关键内容,按需加载 |
| 多语言字体 | 懒加载 | swap | 根据用户语言动态加载 |
嗯,这里要注意一点:预加载和懒加载不是互斥的。你可以对同一个字体文件,在首屏用预加载,在非首屏用懒加载——但这种情况很少见,一般一个字体要么关键要么不关键,不会同时属于两个阵营。
最后说一句:字体加载策略没有银弹。你得根据项目的实际场景——用户网络环境、字体文件大小、页面结构——来灵活搭配。我自己的习惯是:先保证文字可见,再追求字体一致。用户不会因为你用了漂亮的字体而留下,但一定会因为白屏太久而离开。