真正稳的解法是 font-display: optional + 首屏子集字体 + preload + 显式尺寸约束;因为 swap 在移动端会因中文字体体积大、度量差异导致重排抖动,而 optional 彻底规避等待与替换,但需 ≤200KB 子集、preload 及固定容器尺寸才能命中 100ms 窗口。

直接结论:font-display: swap 单独用会加剧抖动,真正稳的解法是 font-display: optional + 首屏子集字体 + preload + 显式尺寸约束。
为什么 font-display: swap 在移动端反而让布局更抖
swap 告诉浏览器“先用系统字体渲染,等自定义字体加载完立刻替换”,但中文字体文件普遍 ≥2MB,在 3G 或弱 Wi-Fi 下常需 2–3 秒才就绪。这期间系统字体(如 PingFang SC、Droid Sans)和你的 Web 字体在 line-height、x-height、字宽上差异极大,浏览器按前者分配空间,替换时重排——标题突然变高、按钮文字换行点跳变、flex 容器内图标错位都是典型表现。
iOS Safari 对 swap 的实现还有延迟,有时替换卡在渲染队列末尾,造成肉眼可见的“闪一下再跳一下”。本地没抖动而线上有,大概率是线上首次加载未缓存 + 网络波动放大了这个过程。
- swap 不解决度量差异,只解决“要不要等”,抖动根源在占位尺寸不准
- 它无法感知加载失败:CDN 404、HTTPS 混合内容被拦截时,页面永远用系统字体,设计师指定的粗体不会出现
- 旧版 iOS Safari(15.4 之前)根本不支持 font-display,直接回退到 FOIT 白屏
font-display: optional 怎么用才生效
optional 的逻辑是:“如果字体能在 100ms 内就绪,就用;否则跳过,全程用后备字体”。它不等待、不替换、不触发重排,首屏文本秒出,彻底规避 FOIT 和 FOUT。
立即学习“前端免费学习笔记(深入)”;
但它不是设了就灵——必须满足三个硬条件:
- 字体文件 ≤200KB:完整思源黑体 Medium 是 8MB+,即使用 optional,浏览器也会尝试加载,而 8MB 在 3G 下不可能 100ms 就绪,结果就是永远 fallback
- 必须
preload:用<link rel="preload" href="/fonts/pfsc-bold-subset.woff2" as="font" type="font/woff2" crossorigin>提前拉取,否则浏览器可能把字体请求排到低优先级 - 只对首屏必需字重生效:比如标题用
font-weight: 600,就只子集化并 preload 这个粗体,别把 Regular / Italic 全塞进去
注意:crossorigin 缺失会导致预加载资源无法被 @font-face 复用,等于白写;as="font" 写成 as="style" 或漏掉,也会降级为普通 fetch。
如何防止后备字体和自定义字体切换时的尺寸突变
即使字体加载成功,不同字体的度量差异仍会引发重排。最有效方式不是靠调参拟合,而是从根上隔离影响:
- 为所有使用自定义字体的元素显式设置
font-size、line-height、height(或min-height),避免依赖字体度量自动计算 - 若必须用
font-display: swap(如品牌字体不可降级),则加size-adjust: 103%微调占位比例——它只在 Chromium 和 Safari 17.4+ 生效,且必须和 swap 共存才起作用 - 回退字体链要收敛:用
font-family: 'YourFont', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif,避免某些 Android 设备 fallback 到 Droid Sans 这类过粗字体
size-adjust 值没有通用解,得实测:在 Chrome DevTools 中 Force font substitution → 开 Layout Shift Regions → 刷新看红框闪烁 → 逐步试 100%、102%、103%,直到红框减弱或消失。
子集化和格式选择的关键细节
中文字体不做子集化,一切优化都白搭。一个“首页”“立即购买”页面实际用到的汉字通常不到 300 个,用 pyftsubset 或在线工具提取后,WOFF2 文件可压到 150KB 左右。
- 别在
@font-face中并列写多个src(如同时写 WOFF2 + WOFF + TTF),移动端解析开销高;单写url('xxx.woff2') format('woff2')最稳妥 - 子集字体必须包含常用标点、数字、英文字母,否则用户输入或动态文案(如搜索框)会回退失败
- 若需支持动态内容,建议“基础子集 + 异步加载全量字体”:首屏用子集,空闲时
fetch全量字体并注入@font-face,后续新渲染文本自动生效
真正容易被忽略的是:字体加载完成后,容器尺寸是否还受字体度量影响——只要没显式设 height 或 min-height,哪怕用了 optional,后续动态插入的文字仍可能因字体差异导致布局偏移。


















