阅读提要

font-display 的四个值决定了一件事:字体没到时,文字是「看不见」「先用系统字体」还是「干脆不换」。选错的表现是白屏或闪烁,而这两种体验的取舍取决于文案长度。

开始阅读

字号

字体为什么会挡住文字

浏览器加载自定义字体时,有三个时间段:

  1. 阻塞期:字体还没下载完,使用该字体的文字不显示。默认最多 3 秒。
  2. 交换期:字体仍然没到,先用后备字体显示,字体到了再替换。默认无限期。
  3. 失败期:超过阻塞期和交换期,浏览器放弃,之后即使字体到了也不换。

如果什么都不配,浏览器走阻塞期——这 3 秒里你的标题是空白的。这就是「白屏」的来源。

font-display 就是用来调整这三个阶段的。

字体加载阶段

四个值的真实差异

block:长阻塞期(约 3 秒),短交换期。文字最多白 3 秒,之后要么换字体要么直接显示后备。

swap:极短阻塞期(约 0-100ms),无限交换期。几乎立刻用后备字体显示,字体到了再换。代价是会闪一下(FOUT,无样式文本闪烁)。

fallback:极短阻塞期 + 短交换期(约 3 秒)。3 秒内到了就换,没到就放弃。

optional:极短阻塞期 + 无交换期。浏览器自己判断——网络慢就直接不用这个字体,本次访问不换。

选择的关键是文字量:

  • 标题、按钮等短文案:swap 的闪烁几乎察觉不到,优先保证不白屏。
  • 正文长文本:整段替换会非常刺眼,optional 或 fallback 更合适。

中文字体的特殊性

一个中文字体文件动辄 3-10MB。这带来两个后果:

一是加载慢。 在 4G 网络下,3MB 大概要 2-5 秒。这段时间里 swap 意味着用户先看到系统字体,然后整页文字重排——非常难看。

二是子集化几乎是必须的。 常用汉字 3500 个左右,覆盖 99% 的日常内容。用工具(fonttools、subfont)把字体裁剪到只含用到的字形,体积能降到 200-500KB。

pyftsubset myfont.ttf \
  --text-file=used-chars.txt \
  --output-file=myfont-subset.woff2 \
  --flavor=woff2

更进一步是按 unicode-range 切片:把常用字、次常用字、生僻字分成几个文件,浏览器只下载需要的部分。Google Fonts 的中文字体就是这么做的。

预加载:把关键字体提到最前

<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>

crossorigin 不能省。字体请求是 CORS 请求,缺了这个属性,浏览器会下载两次(一次 preload、一次实际使用)。

但 preload 要克制:只 preload 首屏真正用到的那一个字体文件。preload 三个字体等于把关键路径堵住,反而更慢。

一个务实的组合

@font-face {
  font-family: 'SiteFont';
  src: url('/fonts/site-subset.woff2') format('woff2');
  font-display: swap;
  unicode-range: U+4E00-9FFF, U+3000-303F, U+FF00-FFEF;
}

body {
  font-family: 'SiteFont', -apple-system, 'PingFang SC', 'Microsoft YaHei', sans-serif;
}

三个要点:

  1. 子集化后的 woff2,控制在 500KB 内;
  2. swap 保证不白屏;
  3. 后备字体链要写全,不同系统落到不同字体,但至少都是清晰可读的。

常见的错误做法

只用 font-display: block 图省事。 结果在弱网下白屏 3 秒,用户以为站点挂了。

把 font-face 写在 CSS 文件里但用 JS 动态插入。 字体请求被推迟到 JS 执行后,首屏必然白字。

用 document.fonts.ready 做淡入。 看似优雅,实际是「先白屏,再淡入」,比直接 swap 更慢。

忽略 local() 回退。 如果用户本地已经装了这个字体(比如系统自带),加一行 local('FontName') 可以完全跳过下载。

检测方法

document.fonts.ready.then(() => {
  console.log('fonts loaded')
})

配合 Performance 面板的「Font」筛选,能看到每个字体文件的下载耗时和触发时机。如果发现某个字体在首屏渲染之后才加载,说明它没有出现在关键路径上——要么 preload,要么接受它晚到。

收尾

字体策略没有统一答案,只有取舍:

  • 想「绝不白屏」→ swap,接受闪烁;
  • 想「绝不闪烁」→ optional,接受部分用户看不到自定义字体;
  • 想「精确控制」→ 子集化 + preload + swap,把文件压到足够小,闪烁短到察觉不到。

第三条是多数站点的最优解,前提是愿意做子集化这一步。

你认为这篇文章怎么样?

还没有表情反馈

分享文章