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 的核心逻辑和解决路径:

FOUT 与 FOIT 知识体系 字体加载延迟 FOUT:后备字体闪烁 FOIT:不可见文本闪烁 解决方案 font-display (swap / fallback / optional) preload 预加载 提前发起字体请求 后备字体优化 视觉匹配 + optional 子集化 + 内联 Base64 嵌入 CSS 核心原则 先让用户看到内容(后备字体),再追求视觉一致性(自定义字体) 控制加载行为,而不是让浏览器自己做决定

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 的核心内容就这些了。记住一句话:用户永远不应该因为字体加载而等待。控制好加载行为,优化好字体体积,你的页面就能做到「无感切换」。