在线工具集

Web 字体加载策略:FOIT / FOUT / Variable

Web 字体让品牌一致性与排版美感成为可能,但代价是首屏延迟、布局偏移、流量浪费。一份未优化的 Web 字体可拖慢 LCP 1 到 2 秒、贡献 CLS 0.1 到 0.3、消耗几百 KB 到几 MB 流量(中文场景)。本文讲透 2026 年的 Web 字体加载完整方法论:@font-face 语法、font-display 五种取值各自适用什么场景、preload 提示如何配合、可变字体省多少、中文字体如何子集化从 7MB 压到 200KB、Google Fonts 与自托管的取舍、中国大陆访问加速。读完能写出零 FOIT、低 CLS、超低流量的字体加载方案。

1. @font-face 与字体加载基础

@font-face 是声明自定义字体的 CSS at-rule。完整声明包括 font-family(自定义名称)、src(字体文件 URL,可同时声明多种格式)、font-weight、font-style、font-display、unicode-range。浏览器看到这条规则不会立即下载字体;只有在 CSS 计算后发现某个 DOM 节点需要这个 font-family 时才发起下载(懒加载)。

src 中的字体格式按现代度排列:woff2(最佳压缩、所有现代浏览器支持,2026 年 99 覆盖率)、woff(旧 IE 与极老浏览器),ttf/otf(裸字体格式,体积大,只在特殊场景)。生产环境只发 woff2 即可。多格式声明用 src: url(font.woff2) format(woff2), url(font.woff) format(woff);浏览器从前向后选第一个支持的。

字体下载触发条件是「计算样式中存在引用」。这意味着如果字体只用在某个 :hover 或 .modal-open 选择器下,未触发前不下载。利用这点可以做按需加载:把巨型字体只挂在特定 class,触发时才拉取。但更常见的优化是相反方向——主字体太重要,需要尽早开始下载。

2. FOIT 与 FOUT:两种渲染策略的本质

字体下载需要时间。下载期间浏览器面对一个选择:要么等字体下完再绘制文字(FOIT,Flash of Invisible Text),要么先用 fallback 字体绘制、字体到了切换(FOUT,Flash of Unstyled Text)。两种策略各有优劣,font-display 属性就是用来控制这个选择。

FOIT 体验是:页面打开后看到完整布局,但文字位置一片空白;3 秒(旧版浏览器无限期)后字体到达,文字突然出现。最差的情况是用户在前 3 秒看不到任何关键内容(标题、按钮文字、错误提示)。Core Web Vitals 上 LCP 元素是文字时,FOIT 直接让 LCP 飙到 3 秒以上。

FOUT 体验是:页面打开后立即用 fallback(例如系统字体)显示文字,用户能立即阅读;字体到达后文字「跳一下」切换到目标字体,可能伴随宽度变化导致小段布局重排。视觉上不如 FOIT 干净,但内容立即可读,CWV 数据更好。

结论:除非品牌强制要求字体一致性,FOUT 几乎总是更优选择。font-display: swap 是 FOUT 模式,是大多数项目的默认推荐。

3. font-display 五种取值详解

font-display 控制字体加载期间的渲染策略,五种取值。

auto:浏览器默认。Chrome/Edge/Safari 行为接近 block,3 秒不可见后切 fallback 并无限期等待字体加载。是最差选择,新项目应避免。

block:强制最多 3 秒不可见,3 秒内字体未到达则用 fallback 临时显示,字体到达后切换。适用范围极窄:图标字体(看不见 fallback 反而比看到方块字符好)。

swap:立即用 fallback 字体绘制,字体加载完成后无限期切换。是 FOUT 模式,最常用的推荐配置。代价是字体到达后会有「字宽跳变」造成 CLS。

fallback:给 100ms 不可见窗口(极短,多数情况用户感知不到),100ms 内字体未到达则用 fallback;3 秒切换窗口内字体到达就切,否则永远用 fallback。适合品牌一致性次要的字体(logo 区可考虑)。

optional:给 100ms 不可见窗口,之后浏览器根据网络判断是否值得下载——弱网下直接放弃下载,永久 fallback;强网下后台下载并下次访问时使用。性能最优、视觉一致性最差。适合非核心装饰字体。

实战配方:正文与 UI 字体用 swap;图标字体用 block;可有可无的装饰字体用 optional。

4. preload 提示:让关键字体早下 200ms

@font-face 是懒加载——浏览器要等 CSS 解析完且发现 DOM 用到才发起字体请求。在 head 加 link rel=preload as=font 可以让字体在 HTML 解析早期就开始下载,比默认快 100 到 300ms,对 LCP 是文字时尤其有效。

正确写法:link rel=preload href=/fonts/inter-var.woff2 as=font type=font/woff2 crossorigin。注意四个细节:as=font 必须;type=font/woff2 必须(否则浏览器无法区分格式);crossorigin 必须(即使同源,字体请求是 CORS 模式);href 必须与 @font-face src 完全一致(多个版本时)才能复用下载。

滥用风险。preload 会与其他关键资源(CSS、HTML 内联脚本)抢带宽。如果 preload 5 个字体,可能反而拖慢 HTML 与 CSS 到达,得不偿失。原则:只 preload 首屏立即可见的关键字体,通常 1 到 2 个文件(一个英文 + 一个中文,或一个可变字体)。其余字体让浏览器按需懒加载。

组合策略:可变字体 + preload + font-display swap,是 2026 年最佳基础配置。

5. 可变字体:一个文件多种字重

可变字体(Variable Fonts,OpenType 1.8 引入)允许在单个字体文件中包含多个字重、字宽、斜体的连续变化。传统多字重方案需加载 inter-400.woff2、inter-500.woff2、inter-700.woff2、inter-italic.woff2 四个文件共 800KB;可变字体 inter-var.woff2 单文件 350KB 涵盖所有变化,省 55。

使用方式:@font-face 内 src 指向可变字体文件,font-weight 写范围 100 900、font-style 写 oblique 0deg 14deg。CSS 中正常写 font-weight: 600 浏览器即按可变轴插值出对应字重。还可以用 font-variation-settings 直接控制非标准轴:wght(字重)、wdth(字宽)、slnt(倾斜度)、opsz(视觉尺寸)。

主流可变字体:Inter(强烈推荐,UI 字体首选)、Roboto Flex(Google 出品)、Recursive(编程与 UI 双用)、Source Sans 3、Noto Sans(思源系列)、Fraunces(衬线)。中文方面 Source Han Sans VF 也已发布但体积仍较大,需配合子集化。

浏览器支持 2026 年覆盖率超过 95,无需 fallback 担忧。新项目应优先选可变字体。

6. 中文字体子集化:7MB 到 200KB 实战

中文字体的痛点是字数多。GB 18030 包含 7 万多字,常用 GBK 收 2 万多字,单字重未压缩 ttf 可达 15MB,woff2 压缩后也常 4 到 7MB。直接在网页用思源黑体一个字重,移动 4G 用户首屏多等 5 到 10 秒。子集化是唯一现实方案。

方法一:按页实际用字提取(精确子集化)。工具 fonttools 的 pyftsubset 命令、Glyphhanger(扫描 HTML 页面提取所有字符)、cn-font-split(中文专用,开源,自动多文件分包)。例如博客每篇文章构建时扫描所有汉字,生成只含这些字的字体文件,单页 50 到 200KB。

方法二:按 unicode-range 分包(声明式按需)。把字体按汉字分块预切:CJK 基本(U+4E00-U+9FFF)、扩展 A(U+3400-U+4DBF)、标点(U+3000-U+303F)、英文(U+0000-U+00FF)、扩展拉丁等。@font-face 重复声明同一 font-family,每个声明指向一个分包文件并配 unicode-range。浏览器看到 DOM 中的字符按需下载对应分包,未用到的分包永不下载。Google Fonts 的中文字体(如 Noto Sans SC)默认这样做,每个分包约 100KB,全量约 1.5MB 但实际加载只需对应几个分包。

方法三:服务端动态子集化。CDN 后端按 referrer 或 query 参数动态生成只含特定字符的字体文件。适合 CMS 内容多变的场景。开源项目 cn-font-split 与 ZenoZeng 的方案都支持这个模式。

实战推荐:博客与文档站用方法一(构建时精确);社交平台与论坛用方法二(unicode-range 分包);高度动态站点用方法三。配合 Critical CSS 实战 同步内联首屏关键字体子集,可达到首屏文字立即可见、零 FOUT 切换。

7. Google Fonts vs 自托管 vs 国内镜像

Google Fonts 是最简单的方案:在 head 加 link rel=stylesheet href=fonts.googleapis.com/css2?family=Inter 一行接入。优势:自动子集化、woff2、CDN 全球加速、HTTP/2、无需运维。劣势:fonts.googleapis.com 与 fonts.gstatic.com 跨域多一次 DNS+TLS 握手;中国大陆访问极慢或不通;2022 年起德国法院判定 Google Fonts 收集 IP 违反 GDPR,欧洲项目需自托管。

自托管把字体文件放自己 CDN(Cloudflare、阿里云 OSS、AWS S3 + CloudFront)。优势:同域避免跨域开销;中国可用;GDPR 合规;可定制 cache-control(max-age 1 年没问题,文件 hash 改名做版本控制)。劣势:要手动下载 woff2、做子集化、写 @font-face。Next.js 的 next/font/google、Astro 的 astro:assets 把这套自动化——构建时下载字体、子集化、生成 @font-face、内联 CSS,开发体验等于 Google Fonts,效果等于自托管。

中国市场专用方案:75CDN(七牛字体镜像)、中科大开源镜像、字节跳动镜像,把 Google Fonts 镜像到国内 CDN,URL 替换即可。但稳定性与隐私不一定可控,最稳是自托管到自己国内 CDN。

结论:欧洲项目(GDPR)必自托管;中国项目必自托管;其他项目优先 next/font / astro:assets 这类构建时自动方案。

8. 减少 FOUT 字宽跳变:size-adjust + ascent-override

FOUT 模式下字体到达瞬间有「字宽跳变」造成 CLS。Chrome 87 起支持 @font-face 的四个度量调整属性:size-adjust、ascent-override、descent-override、line-gap-override。它们让 fallback 字体的渲染度量与目标字体接近,切换时跳变最小。

实战做法:用 Fontaine、Fontkit、capsize 等工具自动测量目标字体度量,生成对应的 fallback 调整 @font-face 声明。例如 Inter 的 fallback 用 system-ui 调整 size-adjust: 100.06 与 ascent-override: 90 等数值,让 system-ui 看起来字宽几乎与 Inter 一致。Next.js 的 next/font 默认应用这套,无需手动调。

Astro 等其他框架可手动用 fontaine npm 包,构建时自动生成调整 CSS。配合 font-display: swap 与精准 fallback 调整,CLS 从 0.15 降到 0.01 以内是常态。

这套体系搭好后,Web 字体不再是 CWV 通过率的拦路虎,而是品牌体验的加分项。

常见问题

FOIT 与 FOUT 哪个体验更好?

FOUT(Flash of Unstyled Text)几乎总是优于 FOIT(Flash of Invisible Text)。FOIT 期间用户看到一片空白文字,关键内容不可读,体验严重;FOUT 让浏览器先用 fallback 字体显示文字,下载完成后切换到目标字体,最差也能看到内容。CWV 阈值上 FOIT 默认 3 秒空白会让 LCP 飙升。font-display: swap 是 FOUT 模式,是大多数场景默认选择;强调品牌一致的极少数场景才考虑 optional 或 block。

font-display 五种取值怎么选?

auto 是浏览器默认,行为接近 block,多数浏览器先 3 秒不可见再无限等。block 强制最多 3 秒不可见再用 fallback。swap 立即用 fallback、字体加载完无限期切换,是默认推荐。fallback 给 100ms 不可见 + 3 秒切换窗口,超时则永远 fallback,适合 logo 文字。optional 给 100ms 不可见,看网络好坏决定是否切换,弱网下根本不下载,是性能最优但视觉一致性最差的选择,适合非品牌字体。

中文字体真的能从 7MB 压到 200KB 吗?

能。中文字体如思源黑体单字重 4 到 7MB,包含 2 万多字。但实际单个网页用到的汉字通常不到 1000 个。子集化(subsetting)按页面实际用字提取所需字形,配合 unicode-range 让浏览器只在需要时下载。工具:fonttools 的 pyftsubset 命令、cn-font-split(开源中文专用)、Sealdog/Glyphhanger。配合按字形分包(CJK 统一汉字、扩展 A、扩展 B 各分一段),200KB 内涵盖常用汉字完全可行。Google Fonts 的中文字体已默认这样做。

可变字体能省多少?

可变字体(Variable Fonts)把多种字重、字宽、斜体打包在一个文件里。传统做法 Inter 字体加载 4 个字重 4 个文件共 800KB;用 Inter 可变字体单文件 350KB 涵盖所有字重,省 55。结合 CSS font-variation-settings 可以无极调整字重(不限于 100/400/700)。代价是浏览器解析与渲染稍慢但可忽略。Inter、Roboto Flex、Recursive、Source Sans 3 都已发布可变版本,新项目优先选可变字体。

Google Fonts 与自托管字体哪个好?

看场景。Google Fonts 优势:CDN 全球加速、自动子集化(unicode-range)、自动 woff2、HTTP/2 多路复用。劣势:跨域第三方域名(fonts.googleapis.com 与 fonts.gstatic.com)多一次 DNS 与 TLS 握手;中国大陆访问极慢甚至不通;GDPR 合规存在 IP 收集争议。自托管把字体放自己 CDN,避免上述问题但需手动子集化、配 cache-control。中国市场基本必须自托管或用阿里字体 / 75CDN 镜像。Next.js 13+、Astro 内置的字体优化(next/font)实质就是构建时下载并自托管。

相关工具