4、FOUT 与 FOIT 现象:无样式文本闪烁与不可见文本闪烁的成因与解决方案
你有没有遇到过这种情况?打开一个页面,文字先是「唰」地闪了一下,或者干脆白屏几秒才出现文字。嗯,这大概率就是 FOUT 或 FOIT 在作怪。
这两个缩写,说白了就是字体加载过程中的两种「尴尬瞬间」。我早期做项目时,经常被设计师追着问:「为什么我的字体没出来?」「为什么文字闪了一下?」——那时候我对这两个概念也是一知半解。后来踩了不少坑,才真正搞明白它们的底层逻辑。
核心概念速览
- FOUT(Flash of Unstyled Text):无样式文本闪烁。先显示后备字体,等自定义字体加载完成后,再「闪」成目标字体。
- FOIT(Flash of Invisible Text):不可见文本闪烁。字体加载期间,文字完全不可见,加载完成后才突然出现。
4.1 为什么会出现 FOUT 和 FOIT?
根源其实很简单:浏览器不知道你要用的字体什么时候能下载完。它面临一个选择——是先用后备字体占位,还是干脆不显示?
不同浏览器的选择不一样。我整理了一张表,你可以直观对比一下:
| 浏览器 | 默认行为 | 最大隐藏时间 | 超时后行为 |
|---|---|---|---|
| Chrome / Edge | FOIT(隐藏文本) | 3 秒 | 降级为后备字体,加载完成后切换 |
| Firefox | FOIT(隐藏文本) | 3 秒 | 降级为后备字体,加载完成后切换 |
| Safari | FOIT(隐藏文本) | 无限制 | 一直隐藏,直到字体加载完成 |
| IE11 | FOUT(显示后备字体) | 无 | 立即显示后备字体,加载完成后切换 |
看到 Safari 那行了吗?无限制隐藏——这意味着如果字体加载失败,用户可能永远看不到文字。我在一个移动端项目中就遇到过这种情况,Safari 用户反馈页面白屏,排查了半天才发现是字体 CDN 挂了,文字被「藏」起来了。
4.2 FOUT 与 FOIT 的「用户体验账」
你可能会想:FOUT 看起来更糟糕啊,文字闪来闪去的。但实际用户体验研究告诉我,FOUT 反而比 FOIT 更「友好」。
为什么?因为用户的眼睛对「变化」更敏感,但对「等待」更焦虑。
- FOUT:用户先看到了内容(虽然是后备字体),可以开始阅读。字体加载完成后,文字「闪」一下变漂亮了。用户甚至可能没注意到这个变化。
- FOIT:用户盯着空白区域,不知道页面是不是卡死了。3 秒后突然蹦出文字,这种「惊吓」感其实更影响体验。
我个人习惯用一句话总结:「先看到,再变好看」永远比「先看不到,再突然看到」要好。
4.3 解决方案:从源头到策略
解决 FOUT 和 FOIT,核心思路就一条:控制字体加载的行为,不让浏览器自己做决定。
4.3.1 使用 font-display 属性
这是最直接、最推荐的方式。在 @font-face 声明中加上 font-display,告诉浏览器你希望它怎么表现。
@font-face {
font-family: 'MyCustomFont';
src: url('/fonts/myfont.woff2') format('woff2');
font-display: swap; /* 关键就在这里 */
}
font-display 有五个可选值,我列个表方便你对比:
| 值 | 行为描述 | 适用场景 |
|---|---|---|
auto |
浏览器默认行为(通常是 FOIT) | 不推荐,等于没控制 |
block |
短时间隐藏文本(约 3 秒),然后降级 | 品牌字体,必须显示 |
swap |
立即显示后备字体,加载完成后切换 | 最常用,兼顾体验 |
fallback |
短时间隐藏(约 100ms),然后降级 | 希望减少闪烁,但又不愿等太久 |
optional |
短时间隐藏(约 100ms),如果没加载完就用后备 | 对网络依赖低,字体可有可无 |
我的建议:大多数项目直接用 font-display: swap 就行。它让用户先看到内容,体验损失最小。如果你对品牌字体有执念,可以用 block,但要做好 3 秒白屏的心理准备。
4.3.2 预加载关键字体
光靠 font-display 还不够,你还需要让字体尽早开始下载。用 <link rel="preload"> 可以做到:
<link rel="preload" href="/fonts/myfont.woff2" as="font" type="font/woff2" crossorigin>
把这个标签放在 <head> 里,浏览器解析到它时就会立即发起字体请求,比等 CSS 解析到 @font-face 再下载要快得多。
我曾经在一个首屏渲染项目中做过测试:加上 preload 后,字体加载完成时间从 1.8 秒降到了 0.6 秒。FOUT 的「闪烁窗口」被大幅压缩,用户几乎感觉不到变化。
注意:preload 不要滥用。只预加载首屏会用到的字体文件,否则会抢占带宽,反而拖慢页面加载。
4.3.3 使用 font-display: optional + 后备字体优化
如果你对自定义字体没那么执着,或者你的用户网络环境比较差(比如移动端),可以考虑 optional 策略。
它的逻辑是:给字体 100ms 的加载时间,如果没加载完,就直接用后备字体,不再切换。这样完全避免了 FOUT 和 FOIT。
@font-face {
font-family: 'MyCustomFont';
src: url('/fonts/myfont.woff2') format('woff2');
font-display: optional;
}
body {
font-family: 'MyCustomFont', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
但这里有个坑:后备字体必须和自定义字体在视觉上尽量接近。否则用户看到的是两种完全不同的字体风格,体验反而更差。
我建议你选后备字体时,注意这几个维度:
- 字宽(font-stretch):窄体配窄体,宽体配宽体
- 字重(font-weight):粗细要匹配
- x-height:小写字母的高度,影响阅读节奏
4.3.4 字体子集化 + 内联
这是「终极方案」——把字体文件变小,小到可以内联到 CSS 里。
用工具(比如 glyphhanger 或 fonttools)提取页面中实际用到的字符,生成子集字体。如果子集足够小(比如只包含英文和数字,几十 KB),可以直接用 Base64 内联到 CSS 中:
@font-face {
font-family: 'MyCustomFont';
src: url('data:application/x-font-woff;charset=utf-8;base64,...') format('woff2');
font-display: swap;
}
这样做的好处是:字体加载和 CSS 解析同步完成,完全没有 FOUT 和 FOIT。坏处是:CSS 文件会变大,而且每次修改字符集都要重新生成。
适用场景:首屏只有少量文字(比如标题、Logo),且对字体一致性要求极高。我一般在 Landing Page 或品牌官网的 Hero 区域用这个方案。
4.4 知识体系总览
下面这张 SVG 图帮你梳理了 FOUT 与 FOIT 的核心逻辑和解决路径:
4.5 避坑指南
最后分享几个我踩过的坑,希望能帮你少走弯路:
- 不要只依赖
font-display: swap:它只是「控制行为」,不解决「加载慢」的问题。配合 preload 和字体压缩才是正解。 - 小心 Safari 的「无限 FOIT」:Safari 对
font-display的支持比较晚(iOS 15+ 才完整支持),老版本 Safari 会无限隐藏文本。建议加一个@supports兜底:
@supports not (font-display: swap) {
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
}
- 字体文件不要太大:单个 woff2 文件超过 200KB 就要警惕了。考虑子集化或换字体。
- 测试网络慢的情况:Chrome DevTools 里模拟 Slow 3G,看看 FOUT/FOIT 的实际表现。我曾经在 2G 网络下测试,发现
font-display: swap的闪烁窗口长达 5 秒——这已经不可接受了。
嗯,关于 FOUT 和 FOIT 的核心内容就这些了。记住一句话:用户永远不应该因为字体加载而等待。控制好加载行为,优化好字体体积,你的页面就能做到「无感切换」。