根本原因其实并不复杂:浏览器并未将 preload 请求正确识别为字体资源。要么是 as="font" 属性写错了或遗漏了,要么是 crossorigin 属性没有添加,再不然就是 href 地址与 @font-face 中的 src 路径不匹配。这三个坑只要踩中任何一个,你辛辛苦苦写的 preload 标签就形同虚设。

为何写了字体 preload,FOIT/FOUT 却依然存在
相信不少开发者都遇到过这个现象:明明在 HTML 中加入了 ,但页面首次渲染时,字体仍然一闪而过,FOIT(Flash of Invisible Text)或 FOUT(Flash of Unstyled Text)持续出现。问题究竟出在哪里?
第一个常见陷阱:as="font" 属性未正确设置。很多人误以为写个 as="woff2" 就够了,甚至直接省略该属性。但浏览器面对这种写法,只会将其当作普通资源请求处理,完全不会按照字体加载的逻辑去运作。
第二个容易被忽略的漏洞:crossorigin 属性缺失。即使字体文件与你的 HTML 页面部署在同一个域名下,浏览器加载字体时也默认采用匿名 CORS 模式。如果你漏掉这个属性,即便字体文件被成功下载,浏览器也不会将其缓存给 @font-face 使用。这不是浏览器的 Bug,而是出于安全策略的必要限制。
第三个隐藏最深的雷区:href 路径不一致。这一点最容易被忽视。你在 preload 标签里写的路径,必须与 @font-face 的 src 完全一致——包括协议、域名、路径,甚至连查询参数(如 ?v=2.1)都绝不能差一毫。只要多一个字符或少一个字符,浏览器就不会复用缓存,导致预加载白费力气。
如何借助开发者工具验证?打开 Network 面板,筛选字体请求:如果 Initiator 列显示的是 parser 而非 preload,或者 Priority 显示 Low,甚至状态为 cancelled,那么基本可以判定预加载并未生效。
字体 preload 的最小可行配置写法
你无需堆砌纷繁复杂的属性,只需精炼到恰到好处即可。下面这份配置是我长期实践后认为最干净、最可靠的写法,专门面向 WOFF2 格式:
对应的 CSS 中,必须有一模一样的 @font-face 声明:
@font-face { font-family: 'Inter'; font-weight: 700; src: url('/fonts/inter-bold.woff2') format('woff2');}
这里面有几个关键要点:
- 加上
type="font/woff2"总是有益的,可以避免浏览器因 MIME 类型探测失败而中断请求。当然,前提是你的服务器能正确返回Content-Type: font/woff2响应头。 - 不要在这个标签上额外添加
onload属性或任何 JavaScript 控制逻辑。字体无需人工“激活”,只要@font-face注册成功,浏览器在渲染到使用该字体的文字时,自然就会从缓存中取用。 - 如果你借助了 CDN 分发字体,那么
href必须使用完整的 CDN 域名(例如https://cdn.example.com/fonts/...),不可仅写相对路径,否则跨域问题会让你再度头疼不堪。
哪些字体不值得预加载
千万别犯这个思维定式:以为预加载就是将所有字重都一股脑儿地预取一遍。事实恰恰相反,预加载是精准狙击,而非地毯式轰炸。你多 preload 一个文件,就多占用一次 TCP 连接,多消耗一份带宽,多挤占其他真正关键资源的加载优先级。
- 只预加载首屏渲染时立刻需要用到的字体组合。比如正文采用
font-weight: 400,标题采用font-weight: 700,那么你只需 preload 这两个 WOFF2 文件即可。其余字重,等真正用到时再按需加载。 - 避免预加载那些可能永远不会在首屏出现的变体,比如
italic、light、thin等。除非你的设计稿首屏确实使用了它们。 - 不要给 Google Fonts 那种全家桶式的
family=Inter:wght@100..900做批量 preload。将其拆解为独立的href,只选择你实际会用到的权重值。 - 如果字体 URL 是动态生成的(例如带有时间戳或哈希值),必须确保这个地址与
@font-face中声明的地址完全一致,否则缓存永远无法命中。
如何确认字体 preload 真正生效
不要仅凭直觉判断,必须动手验证。打开 Chrome 开发者工具,切换到 Network 面板,重点关注三个关键列:Initiator、Priority、Time。
- 筛选条件设置为
type: font,然后观察Initiator这一列,找到preload字样。如果显示的是parser或script,说明预加载根本没有走通。 - 查看
Priority列,正常情况下应该是Highest(Chrome)或High(Firefox)。如果显示Low,那么大概率是as属性写错或缺失了。 - 对比未加 preload 时该字体的 Start Time。理想情况下,添加 preload 后请求发起时机应该提前 200–600 毫秒,尤其在不稳定的弱网环境下差异更明显。如果时间变化微乎其微,则可能是路径不一致,或服务端未配置正确的 CORS 头(
Access-Control-Allow-Origin: *)。 - 最容易被忽略的一种静默失效:查看请求的 Headers 标签页,确认
Accept请求头是否为font/woff2?响应头中的Content-Type是否也与之匹配?只要有一个不匹配,预加载的结果就付诸东流。
还有一个极其隐蔽的坑:服务器返回了 302 重定向,或者设置了 Cache-Control: no-store。这会导致预加载的响应无法被后续渲染复用,同时不会抛出任何错误提示,你只会看到字体被反复下载,性能持续受损。这种静默失效往往让人防不胜防。
