4、FOUT 与 FOIT:字体闪烁现象原理、字体加载超时控制、字体交换策略
你有没有遇到过这种情况?页面打开后,文字先是消失了一两秒,然后突然「啪」一下全出来了。或者更糟——文字先以一种字体显示,等 Web 字体加载完后又突然变了个样子,整个页面像在「闪屏」。
嗯,这就是我们今天要聊的 FOUT 和 FOIT。说白了,它们就是浏览器在加载自定义字体时,两种不同的「等待策略」导致的视觉问题。我在好几个项目中都被这两个问题坑过,尤其是那些对首屏体验要求极高的营销页面。
4.1 什么是 FOUT 和 FOIT?
先搞清楚这两个概念。它们都是浏览器处理 Web 字体加载时的行为模式:
| 术语 | 全称 | 中文解释 | 视觉表现 |
|---|---|---|---|
| FOUT | Flash of Unstyled Text | 无样式文本闪烁 | 先用后备字体显示文字,Web 字体加载完成后突然切换 |
| FOIT | Flash of Invisible Text | 不可见文本闪烁 | 文字先隐藏(不可见),Web 字体加载完成后才显示 |
你想想看,这两种情况哪个更糟糕?我个人觉得 FOIT 更影响用户体验——用户盯着空白区域,完全不知道这里有没有内容。而 FOUT 至少还能看到文字,虽然字体变了,但信息是可读的。
核心区别一句话:FOUT 是「先显示再换字体」,FOIT 是「先隐藏再显示」。前者是视觉跳跃,后者是内容缺失。
4.2 浏览器为什么会有这两种行为?
这就要说到浏览器的字体加载策略了。不同的浏览器,默认行为不一样:
- Chrome / Edge(Blink 内核):默认采用 FOIT 策略。字体加载期间,文字会隐藏最多 3 秒。3 秒内字体加载完成,正常显示;超过 3 秒,则使用后备字体显示,字体加载完成后不再切换。
- Firefox:默认也是 FOIT,但超时时间也是 3 秒。
- Safari:默认 FOIT,超时时间无限——也就是说,如果字体一直加载失败,文字就一直不可见。这曾经是个大坑。
- IE / 旧 Edge:默认采用 FOUT 策略。先显示后备字体,等 Web 字体加载完再切换。
为什么会这样?其实浏览器厂商也在纠结。Chrome 团队认为「先隐藏再显示」能避免页面布局抖动(CLS),而 IE 团队则认为「先显示内容」更重要。没有绝对的对错,但作为开发者,我们需要主动控制这种行为。
我的建议:不要依赖浏览器的默认行为。用 font-display 属性主动声明你想要的策略,把控制权拿回来。
4.3 font-display:控制字体交换策略
font-display 是 CSS 中专门用来控制字体加载行为的属性。它定义了一个字体在加载过程中的「时间线」,分为三个阶段:
- 阻塞期(Block Period):字体未加载完成,浏览器使用「不可见」的后备字体占位(即 FOIT 行为)。
- 交换期(Swap Period):字体仍未加载完成,但浏览器开始使用可见的后备字体显示文字(即 FOUT 行为)。
- 失败期(Failure Period):字体加载失败,浏览器永久使用后备字体。
font-display 有五个可选值,每个值对应不同的时间线策略:
| 值 | 阻塞期 | 交换期 | 失败期 | 适用场景 |
|---|---|---|---|---|
auto |
浏览器默认(通常 3s) | 无限 | 无限 | 不主动控制,交给浏览器 |
block |
无限 | 0s | 无限 | 图标字体、品牌字体(必须显示) |
swap |
极短(约 0.1s) | 无限 | 无限 | 内容型页面,优先保证可读性 |
fallback |
极短(约 0.1s) | 约 3s | 无限 | 兼顾体验与性能,推荐大多数场景 |
optional |
极短(约 0.1s) | 0s | 无限 | 弱网环境,字体加载失败也不影响体验 |
我曾经踩过的坑:在一个移动端 H5 项目中,我用了 font-display: swap,结果字体文件 2MB 没做压缩,用户在弱网下每次刷新页面都会看到明显的 FOUT。后来改成 fallback,配合字体子集化,问题才解决。记住:swap 不是万能药,它只是把 FOIT 变成了 FOUT,但 FOUT 本身也需要优化。
4.4 代码实战:如何配置 font-display
使用方式很简单,在 @font-face 声明中加上 font-display 属性即可:
@font-face {
font-family: 'MyCustomFont';
src: url('/fonts/myfont.woff2') format('woff2'),
url('/fonts/myfont.woff') format('woff');
font-weight: 400;
font-style: normal;
font-display: fallback; /* 推荐大多数场景 */
}
如果你使用 Google Fonts 或第三方 CDN,可以在 URL 参数中指定 display:
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;700&display=swap" rel="stylesheet">
注意这里的 display=swap 是 Google Fonts 的默认值。我个人习惯在引入时改成 display=fallback,除非这个字体对品牌展示至关重要。
4.5 字体加载超时控制
除了 font-display,我们还可以用 JavaScript 更精细地控制字体加载超时。这在一些对字体切换时机有严格要求的场景下非常有用。
浏览器提供了 FontFaceSet API,可以监听字体加载状态:
// 检测字体是否加载完成
document.fonts.ready.then(function() {
console.log('所有字体加载完成');
// 可以在这里添加一个类,做平滑过渡动画
document.documentElement.classList.add('fonts-loaded');
});
// 设置自定义超时
const timeout = setTimeout(function() {
console.warn('字体加载超时,使用后备字体');
document.documentElement.classList.add('fonts-timeout');
}, 3000); // 3秒超时
document.fonts.ready.then(function() {
clearTimeout(timeout);
});
配合 CSS 可以做到更平滑的切换:
body {
font-family: 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
}
.fonts-loaded body {
font-family: 'MyCustomFont', 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
/* 可以加一个过渡动画,让切换不那么突兀 */
transition: opacity 0.3s ease;
}
一个小技巧:在 fonts-loaded 类生效时,给文字加一个 opacity: 0 → 1 的淡入动画,能有效掩盖字体切换时的「闪一下」问题。我在一个电商首页用过这个方案,用户几乎感知不到字体变化。
4.6 知识体系:字体加载策略全景图
下面这张图总结了 FOUT、FOIT 以及 font-display 各值之间的核心关系:
4.7 避坑指南:我踩过的那些坑
- 不要对全部字体用
swap:尤其是中文字体,文件大、加载慢,每个页面都 FOUT 一次,用户会疯掉。我建议只对关键字体(如标题、品牌字)用swap,正文用fallback或optional。 - 注意
font-display的浏览器兼容性:虽然现代浏览器都支持了,但如果你需要兼容 IE11,它是不认这个属性的。IE11 默认就是 FOUT 行为,所以反而问题不大。 - 字体加载失败后的降级方案:我曾经遇到过一个 case,字体 CDN 挂了,结果页面上的文字全部变成了「豆腐块」(空白方块)。后来我在
@font-face中加了多个后备字体,并确保最后一个 fallback 是系统字体。 - FOUT 和 CLS 的关系:字体切换会导致布局偏移(CLS),尤其是不同字体的字宽、字高不一致时。解决办法是给字体设置
size-adjust或使用@font-face的ascent-override等属性来「对齐」度量值。这部分我们后面章节会详细讲。
重要提醒:不要以为加了 font-display: swap 就万事大吉了。它只是解决了「文字不可见」的问题,但「字体切换时的视觉跳跃」依然存在。真正的优化是:字体文件足够小 + 加载时机足够早 + 切换动画足够平滑。这三者缺一不可。