一句话总结
@font-face 是一条 CSS 规则,它允许浏览器下载并渲染自定义字体,终于把设计师从每台电脑预装的那五六款“Web 安全”字体的魔爪中解放出来了。
它解决了什么问题
如果你是在 90 年代末或 2000 年代初做网站,Web 排版简直就是一片荒凉的废墟。你完全受制于用户操作系统里碰巧装了什么字体。你写下 font-family: Arial, Helvetica, sans-serif; 然后开始祈祷。如果设计师丢给你一个 Photoshop 视觉稿,上面用了一种绝美的定制衬线字体做标题,你只有三个糟糕的选项:
- 图片大法 (The Image Hack): 在 Photoshop 里把标题切成图,然后用
<img>标签扔上去。SEO 恨死它了。屏幕阅读器成了瞎子。后来出现的 Retina 屏幕更是把它们糊成了一坨马赛克。 - sIFR (Scalable Inman Flash Replacement): 一个极其邪门的操作。它用 JavaScript 找到文本,把它藏起来,然后嵌入一个极小的 Adobe Flash 动画,用自定义字体把一模一样的文本渲染出来。这玩意又笨重又 Bug 满天飞,而且能把笔记本电池烤化。
- Cufón: 一个把字体路径转成矢量图形然后画在
<canvas>上的 JavaScript 库。挺好看,但你根本没法轻松选定或复制文字。
其实 W3C 早在 CSS2(1998 年)就提出了 @font-face,但那时候浏览器在字体格式上谈不拢,字体厂商又怕死盗版了(要是你把 .ttf 文件放服务器上,谁都能下载)。
直到 2010 年代,Web 开放字体格式(WOFF)——一种专为 Web 压缩和量身定制的格式——被广泛采用,@font-face 才变得具有实用价值。今天,@font-face 可不只是“加载字体”那么简单;它指挥着一场由网络请求、缓存、子集化 (subsetting) 和渲染策略组成的复杂交响乐,在让你的网站快到飞起的同时,完美还原设计师的想法。
底层原理
说白了,@font-face 就是一组指令。它把你瞎编的一个名字跟躺在服务器上的一个文件绑定在一起。
规则剖析
一个现代的、可用于生产环境的 @font-face 声明长这样:
@font-face {
font-family: 'MyAwesomeFont';
src: local('MyAwesomeFont Regular'),
url('/fonts/myawesomefont.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+0000-00FF;
}
我们来拆解一下这坨代码里的魔法。
格式与 WOFF2 霸权
注意这里的 format('woff2')。在黑暗时代,开发者不得不提供一堆庞大且“无懈可击”的兼容包:.eot(给上古 IE 用的)、.ttf(给老旧的 Safari/Android 用的)、.svg(给远古 iOS 用的),还有 .woff。
今天,整个 Web 都跑在 WOFF2 上。它使用了 Brotli 压缩,文件体积比它的老大哥 WOFF 还要小大概 30%。因为 WOFF2 拥有近乎无敌的浏览器支持率(全球超过 97%),你通常只需要扔一个 .woff2 文件就够了——顶多再加个标准的 WOFF 作为 fallback,以防你需要伺候非常老旧的企业古董机。
控制闪烁 (font-display)
当浏览器遇到用自定义字体设置了样式的文本,但字体文件还没下载完时,它必须做个艰难的决定:是用 fallback(后备)字体先顶着呢?还是在自定义字体搞定前直接把文本藏起来?
这就诞生了两个臭名昭著的首字母缩略词:FOUT (Flash of Unstyled Text,无样式文本闪烁) 和 FOIT (Flash of Invisible Text,不可见文本闪烁)。font-display 描述符就是让你来发号施令的。
font-display |
行为表现 | 什么时候用 |
|---|---|---|
auto |
浏览器默认行为(通常会阻塞渲染长达 3 秒)。 | 如果你在乎性能,永远别用。 |
swap |
立即显示 fallback 字体 (FOUT),字体准备好后立马无缝切换到自定义字体。 | 正文、文章、UI 元素。用户可以立即开始阅读。 |
block |
短时间隐藏文本 (FOIT),然后显示 fallback,最后切换。 | 图标或 Logo,因为用 fallback 字体显示出来往往像一堆乱码。 |
fallback |
极短的隐藏期,较短的切换期。如果下载太久,这次页面加载就直接放弃自定义字体了。 | 弱网环境下的装饰性文本。 |
optional |
极其短暂的隐藏期。如果字体没在缓存里或者网速不是快到离谱,它干脆彻底放弃。 | 那些“有它更好,没有也行”且布局稳定性压倒一切的字体。 |
用 unicode-range 做子集化
一个包含拉丁文、西里尔文、希腊文以及复杂的 CJK(中日韩)字符的完整字体文件,随随便便就有好几兆。
unicode-range 属性是一个极其天才的优化工具。它告诉浏览器:“这个字体文件只包含这些特定的字符哦。”
如果你设置了 unicode-range: U+0000-00FF(基本拉丁文),浏览器在解析 HTML 时,如果没有找到任何拉丁字符,它甚至连网络请求都不会发。这让你能把庞大的字体文件切成一口一个的小语种碎片,按需加载用户真正需要的部分。
可变字体 (Variable Fonts)
在以前,如果你想要 Regular (常规)、Italic (斜体)、Bold (粗体) 和 Bold Italic (粗斜体),你得加载四个单独的 .woff2 文件。
可变字体 把整个字体家族打包进了一个文件,通过定义“变化轴 (axes of variation)”来实现。不再局限于像 400 或 700 这样的静态字重,可变字体能渲染出 432 甚至 781 这种刁钻的字重。
@font-face {
font-family: 'MyVariableFont';
src: url('/fonts/myfont-var.woff2') format('woff2');
font-weight: 100 900; /* Accepts any weight between 100 and 900 */
}
Base64 嵌入 vs. 预加载 (Preloading)
有时候,网络请求本身就是瓶颈。你有两个高阶战术:
- 预加载 (Preloading): 在 HTML 的
<head>里加上<link rel="preload" href="/fonts/myfont.woff2" as="font" type="font/woff2" crossorigin>,告诉浏览器立刻马上去抓取这个字体,甚至在下载 CSS 之前就动手。 - Base64 嵌入: 你可以把二进制字体文件转成一坨巨大的文本字符串,然后直接糊进
@font-face规则的url()里。这从根本上消灭了网络请求,但会让 CSS 文件体积膨胀大概 33%。而且在 CSS 下载完之前它会阻塞整个页面的渲染,所以这招严格仅限于极小的、极致优化的子集字体(比如一个自定义 Logo)。
真实世界的实战故事
隐形的电商按钮
一家精品电商网站决定在用户界面里用一套自定义图标字体。他们写了一个标准的 @font-face 声明,但没加 font-display。在办公室飞快的 Wi-Fi 下,一切看起来美如画。
然而,用 3G 网络逛网站的移动端用户正以创纪录的速度流失。当开发小哥模拟弱网环境时,罪魁祸首找到了:浏览器在等图标字体加载完,才肯渲染“加入购物车”按钮。结果就是长达 4 秒钟的不可见文本闪烁 (FOIT)。用户字面意义上根本就看不见按钮,怎么给公司掏钱?给文本加上 font-display: swap; 并预加载了图标字体后,轻松拯救了他们的转化率。
好几兆的排版惨剧
一个知名的技术博客决定支持国际化。为了确保他们丝滑的排版风格一致,他们引入了一个巨大的 Noto Sans 字体,覆盖了完整的 CJK(中日韩)字符。字体文件将近 6MB。
突然,在美国读纯英文文章的用户开始吐槽加载速度像龟爬。为了看一篇关于 JavaScript 的文章,他们的浏览器居然在后台疯狂下载 6MB 的日文和韩文字符。后来,团队用上了 unicode-range。他们把字体拆成了一个 30KB 的拉丁文子集和一个 5.9MB 的 CJK 子集。现在浏览器会自动只给英语读者请求那个 30KB 的文件,性能暴跌的毛病瞬间治好了。
疯狂抽搐的重构
一家设计机构搞出了一个惊艳的编辑排版布局,用上了一款经典无衬线字体的 9 种不同字重。开发老哥任劳任怨地导出了 9 个不同的 .woff2 文件,并写了 9 条 @font-face 规则。
由于浏览器是异步下载文件的,页面在加载时就像帕金森发作一样。300 的字重先出来,接着是 700,然后又是 400,导致整个布局在一秒钟内疯狂重绘、重新计算文本折行四五次。后来,这位老哥把 9 个文件换成了一个单一的“可变字体”(.woff2-variations)。这个单一文件的体积比之前 9 个加起来还要小 40%,只需要一个网络请求,并在一次丝滑的绘制中就把页面渲染出来了。
常见错误与陷阱
- 给现代浏览器喂上古字体格式: 赶紧停掉
.ttf、.eot和.svg,除非你有非常明确的、数据支撑的理由要去兼容 IE8 或者十年前的安卓机。老老实实死磕 WOFF2 就行了。 - 预加载忘了写
crossorigin: 如果你用<link rel="preload">加载字体,你必须加上crossorigin属性,就算字体放在你自己域名下也一样。因为浏览器的安全规范规定,字体必须使用匿名模式 CORS 获取。如果你漏了这个属性,浏览器会把这字体傻傻地下载两遍。 - 把巨大的正文字体做 Base64 编码: 把一个 100KB 的字体直接塞进 CSS 里会让你的 CSS 体积原地爆炸,并直接阻塞渲染。只对极小的字体(比如 5KB 的 Logo 子集)用 Base64。
- 无视 font-display: 依赖浏览器默认行为,基本上就是在惩罚弱网用户。明确地给正文加上
font-display: swap,把“能阅读”的优先级排在“好看”前面。 - 把页面上的每个字体都 Preload: 预加载是一条最高优先级的指令。如果你去预加载一个呆在页面最底部的粗斜体大标题字体,你其实是在抢夺加载页面顶部真正需要的关键 JavaScript 和 CSS 的网络带宽。记住,只预加载“首屏关键 (hero)”字体。
- 过度依赖
local(): 写上local('Helvetica')是在告诉浏览器如果本地有这个字体就直接用。但是,Mac、Windows 和 Linux 之间的字体命名规则千奇百怪。你以为的local('Helvetica Neue Bold')在另一个操作系统上可能直接静默翻车。
为什么它值得你花心思
Web 开发就是一场在美学和性能之间永无止境的拔河比赛。你在塑造一个品牌,而排版就是这个品牌的声音。但你同时也在写软件,对于在地铁里信号断断续续的用户来说,每一个 KB 都是人命关天的。
纯手写一套完美的 @font-face 规则是出了名的心智负担重。它要求你对浏览器的各种怪癖、MIME 类型、子集化语法以及压缩格式有极深的了解。一个现代化的生成器绝不仅仅是帮你写两句 CSS 代码;它其实是在帮你编码“最佳实践”。它会自动生成 WOFF2 文件,构建可变轴,应用正确的 font-display fallback,并输出完美的 Base64 或 preload 标签来绕过网络瓶颈。它确保当你的应用发声时,浏览器真的知道该怎么念对这些词。
深入探索
- MDN Web Docs: @font-face - 绝对权威的规则语法和兼容性指南。
- W3C CSS Fonts Module Level 4 - 官方规范,详细说明了浏览器是如何实现
@font-face、可变字体和 fallback 机制的。 - Optimize WebFont loading and rendering (web.dev) - Google 官方出的权威指南,讲透了 Web 字体、FOIT 和 FOUT 对性能的影响。
- MDN Variable Fonts Guide - 深入剖析使用可变 Web 字体的语法和优势。
- W3C Specification for font-display - 浏览器底层究竟是如何管理字体渲染时间线的,看这里。