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 中专门用来控制字体加载行为的属性。它定义了一个字体在加载过程中的「时间线」,分为三个阶段:

  1. 阻塞期(Block Period):字体未加载完成,浏览器使用「不可见」的后备字体占位(即 FOIT 行为)。
  2. 交换期(Swap Period):字体仍未加载完成,但浏览器开始使用可见的后备字体显示文字(即 FOUT 行为)。
  3. 失败期(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 各值之间的核心关系:

字体加载策略全景图 浏览器默认行为 Chrome/Safari → FOIT IE/旧Edge → FOUT font-display 属性 开发者主动控制 覆盖浏览器默认行为 五个可选值 auto / block / swap fallback / optional 控制阻塞期/交换期时长 视觉表现差异 FOIT:文字先隐藏 → 字体加载完成 → 显示 | FOUT:后备字体先显示 → 字体加载完成 → 切换 策略一:优先品牌 使用 block 宁可等待也要显示正确字体 策略二:平衡体验 使用 fallback 短时等待 + 有限交换 策略三:极致性能 使用 optional 字体加载失败也不影响体验 推荐做法 1. 内容型页面 → font-display: fallback 2. 品牌/图标字体 → font-display: block 3. 弱网/移动端 → font-display: optional + 字体子集化 4. 配合 JS 超时控制 + CSS 过渡动画,平滑切换

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 就万事大吉了。它只是解决了「文字不可见」的问题,但「字体切换时的视觉跳跃」依然存在。真正的优化是:字体文件足够小 + 加载时机足够早 + 切换动画足够平滑。这三者缺一不可。