FlowingDev

@font-face 详解:教浏览器用你的排版语言说话

掌握 CSS @font-face 规则,利用 font-display、预加载、unicode-ranges 和可变字重,高效交付自定义 Web 字体。

试用工具: @font-face 生成器

一句话总结

@font-face 是一条 CSS 规则,它允许浏览器下载并渲染自定义字体,终于把设计师从每台电脑预装的那五六款“Web 安全”字体的魔爪中解放出来了。

它解决了什么问题

如果你是在 90 年代末或 2000 年代初做网站,Web 排版简直就是一片荒凉的废墟。你完全受制于用户操作系统里碰巧装了什么字体。你写下 font-family: Arial, Helvetica, sans-serif; 然后开始祈祷。如果设计师丢给你一个 Photoshop 视觉稿,上面用了一种绝美的定制衬线字体做标题,你只有三个糟糕的选项:

  1. 图片大法 (The Image Hack): 在 Photoshop 里把标题切成图,然后用 <img> 标签扔上去。SEO 恨死它了。屏幕阅读器成了瞎子。后来出现的 Retina 屏幕更是把它们糊成了一坨马赛克。
  2. sIFR (Scalable Inman Flash Replacement): 一个极其邪门的操作。它用 JavaScript 找到文本,把它藏起来,然后嵌入一个极小的 Adobe Flash 动画,用自定义字体把一模一样的文本渲染出来。这玩意又笨重又 Bug 满天飞,而且能把笔记本电池烤化。
  3. 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)

有时候,网络请求本身就是瓶颈。你有两个高阶战术:

  1. 预加载 (Preloading): 在 HTML 的 <head> 里加上 <link rel="preload" href="/fonts/myfont.woff2" as="font" type="font/woff2" crossorigin>,告诉浏览器立刻马上去抓取这个字体,甚至在下载 CSS 之前就动手。
  2. 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 标签来绕过网络瓶颈。它确保当你的应用发声时,浏览器真的知道该怎么念对这些词。

深入探索

理论搞定,动手试试吧——100% 在你的浏览器中运行。

试用工具: @font-face 生成器