生产环境中的可变字体:如何上线而不拖垮性能
一份可变字体生产实战指南。值得上线的四个轴,让文件体积减半的子集化策略,以及让页面不再跳动的加载方案。

生产环境中的可变字体:如何上线而不拖垮性能
可变字体是一个拥有无限字重的文件,用对的团队既省了字节,又解锁了表现力。用错的团队则在加载 800KB 的字体,为了没人会看到的动画阻塞首次绘制。差别不在字体的选择,而在生产策略。
可变字体是一个拥有无限字重的文件
可变字体把所有字重、倾斜和宽度都塞进一个文件里,并提供可以在其间插值的轴,这既是它的全部卖点,也是它的全部代价。一次网络请求取代四到七个静态字体请求。这个文件比任何单个静态字重都大,但比整套静态字重加起来要小。交易就是这样。
这种格式是带有内嵌变化数据的 OpenType。浏览器在渲染时在轴的端点之间插值,这意味着你可以白拿 font-weight: 350 或 font-weight: 723,而不只是字体厂商选择发布的那几个整数。悬停字重动画模式正是因此成为可能,而一个粗心的实现也正是因此让只用两个字重的网站加载了 600KB 的文件。
值得上线的四个轴
字重、倾斜、光学尺寸、宽度。大多数生产站点只需要其中两三个,上线比实际使用更多的轴只是浪费字节。
OpenType 规范定义了五个注册轴:字重(wght)、宽度(wdth)、倾斜(slnt)、斜体(ital)和光学尺寸(opsz)。除此之外,字体厂商还可以定义自定义轴,从字阶到表现力都行。自定义轴在字体样张里很好玩,但在生产环境里几乎从不值得那点文件体积。除非设计明确需要别的,否则就坚持用这几个注册轴。
| 轴 | CSS 属性 | 典型范围 | 何时使用 |
|---|---|---|---|
字重 (wght) | font-weight | 100–900 | 几乎总是 |
宽度 (wdth) | font-stretch | 75%–125% | 编辑类排版、响应式标题 |
倾斜 (slnt) | font-style: oblique Xdeg | -15deg 到 0deg | 替代真斜体或做动画 |
光学尺寸 (opsz) | font-optical-sizing | 6–144 | 一个文件同时覆盖展示字与正文 |

字重是主力
字重轴取代了大多数网站上线的四到七个静态字重文件,光靠它自己就足以让可变字体回本。一个把 Regular、Medium、SemiBold、Bold 和 ExtraBold 都作为静态文件上线的站点,要发出五次字体请求。带字重轴的可变字体只发一次,还白送每一个中间值。
CSS 就是在任意元素上写 font-weight: 300 到 font-weight: 900。像 font-weight: 450 这样的中间值同样有效,并且渲染正确。在假定可变字体覆盖完整的 100 到 900 范围之前,先查字体厂商的轴范围文档。有些字体只提供更窄的范围,并且会悄悄截断,结果是出现细微却难看的阶梯感,而不是平滑过渡。
倾斜解锁真正的斜体
真正的倾斜轴让你可以在任意数值上从正体过渡到斜体,而大多数静态字体配对没有第二个文件就做不到。slnt(倾斜)和 ital(斜体)的区别很重要:倾斜是从正体到倾斜体的连续范围,而斜体是一个二元开关,会切换到另一套经过光学校正的独立斜体设计。
对正文来说,带有合适光学斜体的斜体轴几乎永远是正确选择。对于你打算做动画的标题,倾斜才是你要的轴。CSS 是 font-style: oblique -10deg,角度值对应轴的范围。在确定数值之前,先在你实际的标题字号下测试倾斜。极端的倾斜角度在小字号下看起来比字体厂商的样张所暗示的要糟。
光学尺寸让展示字体保持生命力
光学尺寸会根据渲染字号调整对比度、x 高度和细节,这就是把展示字体和正文字体合并进同一个字体文件的差别所在。为 12px 正文设计的字体,细节简化,字腔开阔,间距宽松。80px 的展示切体则有更锐利的对比、更紧的间距和更精致的细节。光学尺寸让你从一个文件里同时得到两者。
CSS 属性是 font-optical-sizing: auto,它让浏览器根据声明的 font-size 应用合适的光学设置。你也可以手动设置,比如给 72px 的标题写 font-variation-settings: 'opsz' 72。真正支持这个轴的字体,典型例子是 Inter Display 对比 Inter Text,是把光学尺寸当作一等特性来设计的。事后才补上一个 opsz 轴的字体,很少能带来有意义的视觉差异。搞清楚你买的是哪一种。
宽度是编辑类排版的轴
宽度轴让你不用换字体就能压窄标题、拉宽引语,这正是让编辑类版式出彩的那一招。CSS 属性是 font-stretch,取百分比值:font-stretch: 75% 为窄体,font-stretch: 125% 为宽体。
这个轴在拥有响应式编辑版式的站点上值回它的文件体积。桌面端 120px 的标题,到 375px 的手机上可能需要压窄才能放进栏里,而宽度轴只用一条 @media 查询就能搞定,不需要再加载第二个字体。Roboto Flex 在可用于生产的可变字体中提供了最宽的宽度轴范围之一,如果你打算认真使用这个轴,值得研究一下。对于标题宽度固定的营销站点,完全跳过它。
可变还是静态,决策树
当你使用三个或更多字重、给字体做动画,或需要光学尺寸时,可变字体胜出。否则,就用静态。盈亏平衡点通常是两到三个字重,而具体的账会随你的语言覆盖需求而变化。
| 场景 | 最佳选择 | 原因 |
|---|---|---|
| 1–2 个字重,无动画 | 静态 | 总字节更小 |
| 3 个以上字重 | 可变 | 一个文件胜过三个文件 |
| 字重或倾斜动画 | 可变 | 只有可变字体才做得到 |
| 需要光学尺寸 | 可变 | 省掉第二次字体加载 |
| 仅窄范围拉丁文,单一字重 | 静态 | 可变没有好处 |
| 多语言环境,语种众多 | 带子集化的可变字体 | 一个基础文件,按语言扩展 |
一个只上线 Regular 和 Bold 的站点,用两个合计 40 到 60KB 的静态文件,大概比用一个 120 到 200KB 的可变字体更划算。一旦字重超过三个,或者需求里加入了动画,天平就果断倒向可变字体。
子集化让文件体积减半
子集化会丢掉你不用的字形,在营销站点上,这意味着 600KB 的可变字体变成 200KB,而视觉上毫无变化。完整的可变字体包含字体厂商设计的每一个字形:拉丁文、拉丁文扩展、西里尔文、希腊文、数学运算符、货币符号,以及你永远用不到的连字。子集化之后的可变字体只包含你的内容所需要的部分。
这件事的工具是 fonttools 库里的 pyftsubset。一个基础的拉丁文子集命令:
pyftsubset font.woff2 \
--output-file=font-subset.woff2 \
--flavor=woff2 \
--unicodes=U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD \
--layout-features="kern,liga,calt,rlig"
保留 kern、liga、calt 和 rlig 这几个排版特性。其余的全部剥掉,除非设计明确用到。如果工具没有自动处理压缩,就把输出再过一遍 WOFF2 压缩器。

拉丁文、拉丁文扩展与语言范围
正确的子集化策略是:一个拉丁文文件,加上每种语言一个语言扩展文件,仅在需要时加载。大多数营销和产品站点主要提供拉丁文内容。把拉丁文子集作为默认上线,并通过 @font-face 中的 unicode-range 描述符来加载扩展字符集,它会告诉浏览器只有在页面上遇到匹配字符时才去获取对应文件。
@font-face {
font-family: 'YourFont';
src: url('/fonts/yourfont-latin.woff2') format('woff2');
font-weight: 100 900;
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC,
U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074,
U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
U+FEFF, U+FFFD;
}
@font-face {
font-family: 'YourFont';
src: url('/fonts/yourfont-latin-ext.woff2') format('woff2');
font-weight: 100 900;
font-display: swap;
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020,
U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F,
U+A720-A7FF;
}
浏览器只下载与当前页面字符匹配的文件。法语页面会加载拉丁文扩展文件,英语页面则不会。Google Fonts 就是这样大规模分发的,这也是你为自己的可变字体应该照抄的正确模式。
不卡顿的加载
在首次绘制之后才加载的可变字体会让布局发生偏移,加载方案就是 preload 加上 font-display swap,再加上一个度量匹配的后备字体。这不是可选项。可变字体比单个静态字重更大,意味着它到达得更慢,也就意味着布局偏移的窗口更宽。
preload 链接放在 <head> 里,位于所有样式表之前:
<link
rel="preload"
href="/fonts/yourfont-latin.woff2"
as="font"
type="font/woff2"
crossorigin
>
@font-face 中的 font-display: swap 声明告诉浏览器立即用后备字体渲染,等可变字体到达后再换上。可变字体是一个拥有无限字重的文件,而如果你在不同样式表里混用 preload 和非 preload 的声明,错误的加载策略会白白把它加载两次。每个子集文件一个 preload。规则就这么多。
想要字节减半、绝不让页面卡顿的可变字体?雇用 Brainy,我们会为你交付生产级排版方案:选对轴,选对子集,配上让 Lighthouse 保持绿色的加载方案。

度量匹配的后备字体
度量匹配的系统后备字体使用 size-adjust、ascent-override 和 descent-override,让后备字体以与可变字体相同的节奏渲染。没有这一步,font-display: swap 只是用布局偏移换掉了不可见文字,偏移就发生在可变字体到达的时候。有了度量匹配,替换在视觉上就察觉不到。
@font-face {
font-family: 'YourFont Fallback';
src: local('Arial');
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
size-adjust: 107%;
}
具体数值取决于你的可变字体。Fontaine 和 next/font 这样的工具会自动生成这些数值。如果你用 Next.js 构建,next/font 会处理整条后备链。如果不是,Fontaine 是不用手算就拿到正确覆盖值的最快路径。
可变字体解锁的四种动画模式
悬停字重、滚动字重、悬停倾斜和展开宽度。这四种动画只有可变字体才做得到,克制使用时它们值回文件体积。把四种全堆在一个页面上,看起来就是技术演示,而不是设计。
悬停字重在悬停时把 font-weight 从 400 过渡到 700。插值很平滑,因为轴数据就在字体里。元素上写 transition: font-weight 0.15s ease 就是全部 CSS。它适用于导航链接、行动号召文字和交互式标签。
滚动字重通过 JavaScript 滚动监听器,把 scrollY 映射到轴范围内的一个值,从而由滚动位置驱动 font-weight。把范围收紧:300 到 700 看起来是有意为之,100 到 900 看起来像个 bug。它适合用在首屏区块,随着用户滚动进入内容,文字变得越来越重。
悬停倾斜在悬停时把 font-style: oblique 0deg 过渡到 font-style: oblique -10deg。它是悬停字重的更含蓄替代方案,在导航链接和行内交互元素上显得更精致。只有当字体本身被设计成同时支持两者时,才把它与悬停字重组合使用。
展开宽度把 font-stretch 的动画作为入场动画的一部分。文字以 font-stretch: 75% 进入,并在 400ms 内展开到 font-stretch: 100%。配合淡入,它是目前最干净的排版展开模式之一。正文别用。它只属于展示字体和首屏标题。
七个大规模上线可变字体的站点
GitHub、Stripe Press、Vercel、Linear、The New York Times、Apple Music 和 Figma 都在生产环境中使用可变字体,它们共有的模式就是值得照抄的模式。
GitHub 使用 Mona Sans 和 Hubot Sans,两者都是为 GitHub 品牌打造的自定义可变字体。大多数 UI 场景只用字重轴。除了偶尔的营销区块,没有动画。文件体积很紧凑,因为他们按语言组进行了激进的子集化。
Stripe Press 使用自定义的展示型可变字体来做编辑类标题。他们把字重轴用到极致,同一个文件里,展示标题用重字重,说明文字用轻字重。在较大的标题字号上,光学尺寸的效果很明显。这是这份名单里编辑野心最大的可变字体用法。
Vercel 在 Geist Sans 和 Geist Mono 上使用自家的可变字体 Geist。字重轴覆盖完整范围。这款字体是开源的,拉丁文子集化干净,加载速度快到可以作为参考:一个在生产环境 Next.js 应用里上线得当的可变字体应该是什么样子。
Linear 使用 Inter 作为可变字体。字重轴覆盖各种 UI 字重,保持在适合密集 UI 的较窄光学字重范围内。这是名单上最保守的用法,也是对数据密集的产品界面来说最正确的用法之一,因为动画在那里只会是噪音。
The New York Times 使用自定义的可变衬线体作为展示字体。字重轴和光学尺寸轴都处于启用状态,展示字与正文之间的过渡通过光学尺寸轴来切换字符渲染。这已经是编辑类规模的可变字体生产使用所能达到的最精细程度。
Apple Music 使用可变形态的 San Francisco,字重和光学尺寸两个轴都启用。大字与小字之间的轴过渡足够平滑,以至于大多数用户根本没发现这是可变字体,这才是正确的目标。字体应当消融在阅读体验里。
Figma 在整个应用中使用 Inter Display 作为可变字体。如果你正在为产品 UI 做展示字体选择,不妨研究 Figma 如何在小号 UI 字重的 Inter 与大号标题字号的 Inter Display 之间取得平衡。它对字重范围的克制值得直接照抄。
可变字体上线前检查清单
上线可变字体之前跑一遍这个清单,你就能抓住那些在生产环境里咬人的 bug。
- 已验证子集化。 用你的目标 unicode 范围运行
pyftsubset。确认拉丁文子集文件小于 200KB。抽查你的内容所用的字符都在输出中。 - 已记录各个轴。 清楚字体提供哪些轴,以及每个轴的最小值和最大值。如果轴最高只到 700,就不要声明
font-weight: 800。它会悄悄截断,你的设计就会出错。 - 已设置 preload。 每个子集文件一个
<link rel="preload">,放在<head>中,带crossorigin属性,且只指向主要的拉丁文文件。预加载一个你用不到的文件会浪费带宽。 - 已配置 font-display。 每个
@font-face块都有font-display: swap。在 DevTools 的 Network 面板中确认字体没有阻塞任何关键路径资源的渲染。 - 后备字体度量已匹配。 系统后备字体有对应的
@font-face块,并算好了size-adjust、ascent-override和descent-override。在 DevTools 中限速到 Slow 3G,检查加载时的布局偏移来验证。CLS 应该为 0 或接近 0。 - 已确认设计用到的轴。 CSS 中声明的每个轴都在最终设计中真正用到。字体文件里有但 CSS 里没用的轴仍然会增加字节。如果你没用宽度,考虑通过轴范围子集化把那些字节裁掉。
- 已检查 Lighthouse 分数。 在装好字体的生产 URL 上运行 Lighthouse。与字体相关的 LCP 和 CLS 应该是绿色。如果不是,说明后备字体度量不对,或者缺少 preload。
- 已确认 CORS 头。 从 CDN 或外部源加载的可变字体需要
Access-Control-Allow-Origin头。在 Network 面板中检查是否有 CORS 错误。CORS 失败会导致字体悄悄回退,在生产环境里很难诊断。
FAQ
什么是可变字体,它与静态字体有什么不同?
可变字体是一个单独的字体文件,包含一套字体设计的完整范围,由字重、宽度、倾斜和光学尺寸等轴来控制。静态字体则是某一个字重和样式下的单一快照。实际的区别在于,可变字体让你可以在 CSS 里使用 font-weight: 350,或者平滑地给字重做动画,而静态字体只允许使用字体厂商作为独立文件发布的那些特定值。
可变字体真的能提升性能吗?
这取决于你加载了多少个字重。如果你上线的是两个合计 60KB 的静态字重,换成 180KB 的可变字体就是性能倒退。如果你上线的是五个合计 300KB 的字重,换成 120KB 的子集化可变字体就是明显的胜利。可变字体在性能上的优势是真实的,但只有在超过三个字重的门槛并且正确子集化之后才成立。
UI 设计里最好的可变字体是什么?
Inter 是 UI 设计中部署最广泛的可变字体。它的字重轴针对屏幕使用校准得很好,子集化干净利落,Figma 也把它作为生产参考上线。当你需要为响应式编辑版式提供较宽的轴范围时,Roboto Flex 是正确选择。如果你想从同一个字体家族里同时得到可变无衬线体和可变等宽体,Recursive 值得研究。对于需要更具表现力的展示字体的网页设计原则,Fraunces 和 Playfair Display 都提供了出色的光学尺寸轴和字重轴。
如何在 CSS 中使用 font-variation-settings?
font-variation-settings 是直接设置可变字体轴的底层属性,使用四字母的轴标签和数值:font-variation-settings: 'wght' 450, 'wdth' 85。对于字重和宽度这类注册轴,优先使用高层 CSS 属性(font-weight、font-stretch),因为它们能正确继承和组合。只有对没有高层 CSS 属性的自定义轴,才使用 font-variation-settings。
可变字体会导致布局偏移吗?
会,而且比静态字体更容易导致布局偏移,因为文件更大、加载更慢。解决办法是 font-display: swap 加上度量匹配的后备字体。这个组合让替换在视觉上察觉不到。没有度量匹配的后备字体,font-display: swap 只是用字体到达时明显的布局跳动换掉了不可见文字。这两个问题要么一起解决,要么都解决不了。
所有浏览器都支持可变字体吗?
所有现代浏览器都支持可变字体:Chrome、Firefox、Safari 和 Edge。这条基线已经足够牢固,你不需要为了浏览器兼容性准备静态字体后备。对大多数生产站点来说,使用不支持可变字体的浏览器的用户比例低于百分之一。针对这种边缘情况提供系统字体后备就行,而不是一整套静态字体栈。
可变字体的子集化是如何工作的?
子集化会从字体文件中移除你的内容永远不会渲染的字形,使用的是 Python fonttools 库里的 pyftsubset 这类工具。你指定所需的 unicode 字符范围,工具会剥掉其余一切。完整的可变字体可能包含 3,000 个字形,而仅拉丁文的子集保留大约 250 个。对拉丁文内容来说,结果是视觉输出完全相同,文件体积大约只有三分之一。完整命令参考请见子集化的术语表条目。
可变字体真正解锁的转变
可变字体不是排版上的新奇玩意。它们是以更少字节交付更多表现力的方式,而把它们当作生产工具的团队,正是用更低网络成本做出更好排版的团队。
驱动现代设计系统的 Figma variables 架构与可变字体的轴天然对应。Figma 里的一个 font-weight token,就是字体里的一个轴值。把结构良好的 token 系统与正确子集化的可变字体配对,你就能得到从设计文件到生产环境毫无歧义地传递的设计决策。再把它与悬停状态上的 OKLCH 颜色系统配对,你做出的产品交互会让人觉得是深思熟虑的,而不是拼凑出来的。
生产环境可变字体的标准不是“我们上线了一个可变字体”。标准是:子集化正确、preload 恰当、后备字体度量匹配,并且只保留设计用到的轴。这就是把真正做生产排版的团队,与只在做排版表演的团队区分开的标准。
想要字节减半、绝不让页面卡顿的可变字体?雇用 Brainy,我们会为你交付生产级排版方案:选对轴,选对子集,配上让 Lighthouse 保持绿色的加载方案。
Want variable fonts that ship at half the bytes and never jank the page? Brainy ships production typography stacks with the right axes, the right subsets, and the loading recipe that keeps Lighthouse green.
Get Started




