字体加载慢导致文字闪一下:font-display、preload 与中文字体子集化的取舍

文字先空白后跳动,多半不是字体太大,而是 font-display 和 preload 配错了。五个取值的行为差异,加上中文字体子集化的真实成本。

约 8 分钟读完 · 更新于 2026-10-21 · 文中每条都能在下面的配套工具里动手验证

先定性:这是渲染策略问题

字体导致的「闪一下」只有两个来源:字体到达前文字不可见(FOIT),或者先用后备字体画出来、到达后整段替换(FOUT)。两者都不是 bug,而是 font-display 明确规定的行为。选哪个值,本质是在「早看到字」和「字形正确」之间二选一,不可能同时要。顺序上还有一件事比策略更靠前:先把字体文件变小。一个 5MB 的中文字体,无论 font-display 写什么,用户体验都不会好。顺序永远是先减字节,再选策略,反过来做只是把痛苦换个位置。

中文为什么会把问题放大

FOIT 期间浏览器把文字渲染成透明,用户看到的是占位但没有内容;FOUT 先画后备字体,字体到位后触发一次重绘,会引起 CLS,尤其当中文字体与系统字体的字宽差异大时,一行文字可能整体位移几个像素。中文还有一个额外成本:常用汉字 3500 个起,一套完整中文字体动辄 3-10MB,是拉丁字体的上百倍。所以中文站点讨论字体加载,第一优先级永远是减少字节,而不是调 font-display。

font-display 五个取值

取值阻塞时长切换行为适用场景
auto由浏览器决定,通常接近 block按 UA 默认策略执行不建议显式写,行为不可预测
block约 3s3s 内文字不可见,之后用后备字体并无限期等待替换品牌标题、字数极少且必须用指定字形
swap0s立刻用后备字体,字体到达后替换正文,希望首屏立刻可读
fallback约 100ms极短阻塞,随后用后备字体,约 3s 内到达才替换折中方案,减少二次重排
optional约 100ms极短阻塞,首次加载没用上则本次不加载非关键装饰字体,允许首访没有它

一份能直接用的 @font-face

可变字体 + unicode-range + 后备字体度量对齐
@font-face {
  font-family: "InterVar";
  src: url("/fonts/inter-var.woff2") format("woff2");
  font-weight: 100 900;
  font-display: swap;
  unicode-range: U+0000-00FF, U+2000-206F, U+2190-21FF;
}

@font-face {
  font-family: "Inter Fallback";
  src: local("Arial");
  size-adjust: 107.12%;
  ascent-override: 90.49%;
  descent-override: 22.43%;
  line-gap-override: 0%;
}

body {
  font-family: "InterVar", "Inter Fallback", sans-serif;
  font-synthesis: none;
}

五个容易踩的细节

  • preload 必须带 crossorigin:字体请求走 CORS 模式,preload 不写该属性时与真实请求不匹配,浏览器会下载两次。
  • local() 放前面会让不同机器版本不一致:装了旧版 Helvetica 的用户和没装的用户看到的字形不同,设计稿对齐时尤其致命。
  • font-weight 数值要和文件真实范围一致:给一个只有 400 的文件声明 700,浏览器会合成假粗体,笔画糊成一团。
  • 可变字体用一个文件覆盖 100-900,四个静态字重约等于四份体积;但中文可变字体仍然是大文件,子集化省不掉。
  • unicode-range 要按真实字符频率切:GB2312 一级字库 3755 字覆盖日常文本的绝大部分,其余按需再切。

中文字体子集化的现实成本

把 5MB 的中文字体切成一百多个子集、每片 30-60KB,是目前的主流做法:CSS 里写上百条 @font-face,浏览器只为页面实际出现的字符下载对应分片。代价是构建复杂度上升,要扫描内容收集字符、维护切片方案、字体更新后全部重跑。另一个常被忽略的现实是授权:不少中文字体按嵌入方式或访问量收费,子集化并不等于免费商用。如果页面主要是正文和 UI 文案,系统字体栈的收益通常高于自托管中文字体,因为省掉的是整个下载链路而不只是几十 KB。还有一个常被混淆的细节:preconnect 只提前建立连接,不下载文件;真正要抢在解析 CSS 之前发起请求,只能靠 preload。

什么时候自托管仍然值得

如果一个站点的品牌字体本身就是设计的一部分,自托管仍然值得,但要按代价付费。可行的组合是:只保留标题用的一个字重,拉丁字符单独切包,中文按 unicode-range 分片,配 font-display: swap,preload 唯一那个关键文件,再把 size-adjust 对齐到后备字体。这套组合能把首屏字体成本压到几十 KB、把 CLS 压到接近零,代价是维护成本:每加一种语言、每换一次字体,都要重跑子集化、重新测量度量、重新验证 preload 路径。系统字体栈的成本恰好是零,换来的是字形随平台漂移。两边的账都算清楚,再决定要不要付这笔钱。

一个反向案例:完全不加载字体

PXBO 的选择是完全不用 webfont,整站只依赖系统字体栈,不加载任何字体文件。收益可以量化:0 个第三方字体请求、0 字体相关 CLS、没有 FOIT 或 FOUT 需要讨论,文字在 HTML 解析后立即以最终字形出现,也不存在 preload 与 crossorigin 的坑。代价是不同系统字形不同:macOS 走 -apple-system,Windows 落到 Segoe UI,字宽和行高会差几个像素,做像素级设计还原时不好对齐。对一个开发者工具站这是可接受的,用户在意的是输入框和结果,不是字体品牌;内容站或品牌站则未必能接受。这是取舍,不是唯一答案。

系统字体栈,可直接用

零字体请求的 font-family
:root {
  --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    "Helvetica Neue", Arial, "PingFang SC", "Hiragino Sans GB",
    "Microsoft YaHei", sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo,
    Consolas, "Liberation Mono", monospace;
}

body {
  font-family: var(--font-sans);
  font-synthesis: none;
  text-rendering: optimizeLegibility;
}

常见问题

font-display 选 swap 就够了吗?

不够。swap 只是把阻塞降到 0,文件太大仍会长时间重排;先减体积再谈策略。

为什么 preload 的字体被下载了两次?

缺 crossorigin 属性。字体请求是 CORS 模式,preload 不带该属性时与真实请求不匹配。

中文字体可以只提供 woff2 吗?

可以。woff2 在主流浏览器全面支持,体积比 woff 小约三成,不需要再补 ttf 或 otf。

size-adjust 和 ascent-override 有什么区别?

size-adjust 整体缩放后备字体的字形大小,ascent/descent-override 只改行框的上下度量,用来对齐行高。

📂

松开以自动格式化

支持 .json / .sql / .csv / .html / .css / .xml / .yaml 文件