前言

我一直很喜欢在网页里嵌入各种 WebFont - 在线字体;WebFont 可以通过网络在客户端上加载本地没有的字体,从而实现更加美观的页面排版。

一般而言,如果需要加载速度最大化,例如我在做 Google 的首页 - 速度需求大于绝对美观度,这个时候全局应用一个 WebFont 就不是最好的选择——用户只需要最快地打开页面开始搜索,并不需要慢慢欣赏页面本身。在这种情况下,给页面适配一个足够好的 font-family 更加合理。

其次,在中文网页上应用 WebFont 对于速度也是一个挑战:单个英文 WebFont 通常不过几十kb,而中文 WebFont 往往需要几M。拿 Source Sans Pro 和思源黑体比较,在 woff2 压缩后,Source Sans Pro 仅有 56.6kb 大小,而思源黑体为 1.53MB;如果用户的浏览器不够现代,在加载字体上的数据消耗还要高出几倍。

在本文里,我会讨论加载中文字体的几种选择,以及 font-family 的设置。

加载中文字体

对于普通的个人和没有多少预算的团队,使用开源/免费商用字体大概是唯一的选择。GitHub 上的 wordshub/free-font 仓库收集了非常之多能够免费商用的中文字体,在选择字体上的时候可以参考一下;而许多国内的企业也发布了他们自己的中文字体,例如阿里巴巴普惠体OPPOSansMiSansHarmonyOS Sans 等等,也可以是不错的选择。

当然,更加直接的来源是 WebFont 提供商,比如知名的 Google FontsAdobe Fonts。Google Fonts 上有数种开源免费的中文字体可以直接通过 Google 的网络加载,其中包括思源黑体思源宋体。而 Adobe Fonts 提供非常广泛的中文字体的选择,但部分字体需要付费的 Adobe CC 订阅才可以使用。

Google Fonts 的使用

在国外的网络环境下,Google Fonts 的使用不能太简单。直接引用 fonts.googleapis.com 提供的 CSS,然后设置相应的 font-family 即可;得益于英文字体的小体积和 Google 优秀的基建,Google Fonts 的加载速度十分之快。

在加载中文字体上,使用 Google Fonts 上的中文字体,比如思源黑体,无疑十分便捷,而不需要任何的服务端配置。然而,虽然 Google Fonts 早在几年前就已经解析到了国内的 Google 自有机房,但是部分运营商还是会自作主张阻断对 fonts.googleapis.comfonts.gstatic.com 这两个域名的访问,导致访客完全无法加载 Google Fonts

大量国内节点无法解析 fonts.gstatic.com

那么,我们可以使用众所周知的 Google Fonts 反代服务。知名的反代服务有如下几个:

后两个只有全球单点,无法兼顾全球速度,故此忽略。根据我的测试(不具有代表性和普遍性,仅作参考),fonts.loli.netfonts.geekzu.org 加载思源黑体均能在国内达到与 Google Fonts 相近的速度:

CSS Sheet (~30kb)字体文件 (~1.5MB)总耗时
fonts.googleapis.com297ms1108ms1405ms
fonts.loli.net592ms1308ms1900ms
fonts.geekzu.org134ms1509ms1643ms

以上虽然仅为单次、单一事件、单一网络环境的测试,但是联系到反代服务的性质以及节点分布,结果之间具有持续性。

你也可以自己试试:

1.5s 左右的总加载时长,看起来不上不下。有没有更好的方式呢?

有。

75CDN 前端静态资源库

之前在《jsDelivr 备案被吊销后,网页静态资源何去何从》一文中也介绍过 360 旗下奇舞团维护的前端公共 CDN;它完全本地化 cdnjs 的资源,并且提供国内 360 自有 CDN + 海外 CloudFront 的优秀配置。不仅如此,它还提供 Google 字体库的本地化。和上面提到的反代服务不同,75CDN 能够直接提供 Google 字体的本地化 @font-face,让客户端直接加载字体文件本身,而无需经过反代和服务端提供内容这两层壁垒。

https://cdn.baomitu.com/index/fonts 页面里填入 Google Fonts 的 CSS 地址,例如 https://fonts.googleapis.com/css?family=Noto+Sans+SC 从而直接返回下述内容:

/* noto-sans-sc-regular */
@font-face {
font-family: 'Noto Sans SC';
font-style: normal;
font-weight: regular;
src: url('//lib.baomitu.com/fonts/noto-sans-sc/noto-sans-sc-regular.eot'); /* IE9 Compat Modes */
src: local('Noto Sans SC'), local('NotoSans SC-Normal'),
url('//lib.baomitu.com/fonts/noto-sans-sc/noto-sans-sc-regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('//lib.baomitu.com/fonts/noto-sans-sc/noto-sans-sc-regular.woff2') format('woff2'), /* Super Modern Browsers */
url('//lib.baomitu.com/fonts/noto-sans-sc/noto-sans-sc-regular.woff') format('woff'), /* Modern Browsers */
url('//lib.baomitu.com/fonts/noto-sans-sc/noto-sans-sc-regular.ttf') format('truetype'), /* Safari, Android, iOS */
url('//lib.baomitu.com/fonts/noto-sans-sc/noto-sans-sc-regular.svg#NotoSans SC') format('svg'); /* Legacy iOS */
}

如果需要不同字重,也可以以这样的格式输入: https://fonts.googleapis.com/css?family=Noto+Sans+SC:200 ,同样会返回对应字重的字体。

注意这个服务不支持 Google Fonts v2 API 以及一些较新的字体,猜测可能是因为适配和同步的原因,需要手打 v1 API。

在网页的样式表中直接插入上述 @font-face 段落,测试下来速度十分明显。简单粗暴地加载 noto-sans-sc-regular.woff2,1.6MB 的大小用时仅需 988ms,秒杀 Google Fonts 自身,更遑论各个反代服务。

当然,这个所谓“秒杀” 也就仅限于国内的网络环境。使用海外原生网络打开 google-fonts.html,CSS 样式表耗时 71ms,1.5MB 的字体文件仅耗时 393ms,加起来也就 500ms 不到。

你也可以自己试试:https://fonts-23154653213.surge.sh/font-sources/baomitu-fonts.html

Google Fonts 的取舍

Google Fonts v1 和 v2 API 均默认支持字体切片,即按照页面上字符的多样性动态加载更多的字体切片,这样可以在部分场景下节省流量,提升页面加载速度。但是在中文环境下,实测证明即使使用 Google Fonts 的字体切片,往往也需要加载近乎全部的字体切片,以实现字符全面覆盖;在这种情况下,由于 cdn.baomitu.com 提供的服务来自商业级别 CDN 以及成熟的基础建设,直接完整加载字体文件反而是更好的选择。

字符子集

Google Fonts v2 API 同时支持生成字符子集以供直接调用;如果你的需求仅仅是在一两句话上应用某一个字体,可以通过下面的方式加载具体字符的子集,从而避免完整加载字体文件而拖慢页面的加载速度。

https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@500&display=swap&text=等到世界末日的那天偷偷地许愿

woff2 文件的大小仅仅为 4.93kb

相应地,你也可以制作自己的字符子集,而无需依赖 Google Fonts。如果你坚持使用 Google Fonts,可以在 v2 API 里设置 display 参数为 swap 或者 fallback,避免字体加载造成长时间的页面阻塞。

Adobe Fonts 和自行托管

Adobe Fonts 的资源域名 use.typekit.net 全球使用 Akamai 分发,在国内访问极其缓慢,几乎可以忽略。

如果你选择用自己的服务托管自己的字体,记得同时提供多种格式以适配非现代的浏览器。

值得一提的是,阿里巴巴的 iconfont 也提供在线字体服务,提供阿里巴巴普惠体和思源黑体的外链;分发域名是 at.alicdn.com, 全球速度同样十分优秀。

WebFontLoader 和 font-display 属性

WebFontLoader 的用途

前几年,FOIT 和 FOUT 是前端讨论的热点之一。FOIT 是 Flash of Invisible Text(瞬时不可见文本),指的是浏览器在等待字体加载的时候,会使用不可见(空白或透明)的字符占位,造成指定字体加载完成之前页面长时间空白的现象。这样无疑会产生非常不好的用户体验。

而 FOUT 是 Flash of Unstyled Text(瞬时未样式化文本),意味着浏览器优先显示其他可用的字体(本地字体),在远端字体加载完毕后再替换的行为。在这种情况下,几乎不会产生页面空白等待字体加载的情况;在字体加载时,用户将可以用同类相似的本地字体阅读网页上的内容。

Google 和 Adobe 联合开发了 WebFontLoader 这个 JavaScript 库,在当时解决了这个问题。WebFontLoader 可以指定字体加载的来源(Google Fonts,Adobe Fonts 或者 custom),通过往 body 标签添加 wf-loading, wf-activewf-inactiveclass,让开发者决定用户在什么时候看到什么样的 font-family

例如我希望使用 Open Sans 显示我的网页,但又不确定用户会不会因为 Google Fonts 加载缓慢而长时间地看到阻塞中的空白页面,我就可以这样使用 WebFontLoader

<!-- 引用 webfontloader.js -->
<script src="https://ajax.loli.net/ajax/libs/webfont/1.6.26/webfont.js"></script>
<script>
WebFont.load({
google: {
families: ['Open Sans'], // 选择 Open Sans 字族
api: 'https://fonts.loli.net/css', // 手动设置 Google Fonts API,可选,仅支持 v1
},
timeout: 1200
// 设置 timeout 时长,单位毫秒,时间过后字体还没完全加载就自动为 body 加上 .wf-inactive class
});
</script>
<style>
.wf-loading, .wf-inactive {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
}
.wf-active {
font-family: "Open Sans", sans-serif;
}
</style>

<body>
<p>
Hello, Welcome to Huangxin's Blog.
</p>
</body>

在使用 WebFontLoader 之后,页面的加载逻辑如下:

WebFont 未加载/加载中:

  • WebFontLoader 往 body 注入 .wf-loading class
  • CSS 规则里,.wf-loading 应用本地字族,即 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;,故此正文使用上述字族

WebFont 已加载:

  • WebFontLoader 往 body 注入 .wf-active class
  • CSS 规则里,.wf-active 应用 "Open Sans", sans-serif,故此正文字体改变为 Open Sans

WebFont 无法加载:

  • 按照规则,1200ms 后若 WebFont 未完成加载,body class 改为 .wf-inactive,用户端显示无变化。

注意这里全程 font-family 使用的都是已有的字符,浏览器不必等待任何其他字体加载,所以不会存在 FOIT 使页面呈现字体占位符的情况。

font-display

根据 caniuse.com,Chrome 60, Firefox 58 和 Safari 11.1 以后,上述浏览器支持了一项新的 CSS 属性:font-display

参考 https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display

font-display 约束了页面显示外部字体的行为,基于它的加载状态。

font-display 有5个值:

auto
让浏览器决定字体显示行为

block
在短时间内阻塞页面字体显示,给予字体无限长的替换时长

swap
在极短时间内阻塞页面字体显示,给予字体无限长的替换时长

fallback
在极短时间内阻塞页面字体显示,给予字体一个短替换时长

optional
在极短时间内阻塞页面字体显示,不给予替换时长

换人话说就是,block 会导致 FOIT;swap, fallbackoptional会有一个极短的 FOIT 阶段(一般为 100ms),在字体加载好之后替换上去;swap 拥有无限时长的替换阶段,也就是说如果字体100秒以后才加载完成,字体也会在100秒被替换上去。fallback 有一个一般为 3s 的替换时长,限时内字体不加载成功则永远不会替换,而 optional 没有替换时长,在一开始的 100ms 无法加载完成则不会有机会被替换。

font-display 的使用方法如下:

@font-face {
font-family: ExampleFont;
src: url(/path/to/fonts/examplefont.woff) format('woff'),
url(/path/to/fonts/examplefont.eot) format('eot');

font-display: fallback;
}

可以发现,以上 font-display 的使用和 WebFontLoader 的用法一模一样,swap 没有 timeout 设置,而 fallback 则是 ~3000ms 的 timeout。因为这个属性已经广泛地被各种主流浏览器支持,WebFontLoader 也几乎没有被使用的必要了。

尽管 Google Fonts 默认的 font-display 是 swap,考虑到国内特殊的网络环境,我还是建议使用 fallback:如果一个 WebFont 3 秒内无法加载完成,它也没有被加载的必要了。

font-family 的设置

在 CSS 的类里定义它的 font-family,可以定义它显示文字时使用的字体。

如果有一个 font-family:

body {
font-family: "Font A", "Font B", "Font C", sans-serif;
}

浏览器在渲染页面的时候,会按照从前到后的顺序尝试适配字体名称,以应用对应的字体。

如果 Font A 存在,则使用它,如果不存在,则尝试从加载的 CSS 文件中寻找是否有对应的 Font-Face,如果还是没有,则移动到下一个字体,如此反复。

sans-serif 这样的是一个字族集,它包含了一系列相似的字体;sans-serif 的意思是非衬线字体,故此在这个集合里的也都是这个类型的字体,而这个集通常是浏览器或者操作系统定义的。常见的集合还有 serif, -apple-system, BlinkMacSystemFontsystem-ui,分别代表衬线字体,Safari 定义的系统字体,Chrome 定义的系统字体和 Windows(以及可能的一些其他操作系统)定义的系统字体。

那么,在我们写 font-family 的时候,除了到别人的网站上抄一整串下来,我们需要注意:

  1. 字集的分布;英文字体如 Arial 不支持中文,而无论中文英文字体都几乎不支持 emoji,故此我们应该遵循 英文字体-中文字体-emoji 字体 的顺序
  2. 字体的选择;在同一个类型的字体里,把字体按它们的理想度自高到地排序,或者按照字体质量的高低排序
  3. 集合的使用;列举完所有的具体之后,我们应该用集合结束 font-family 确保总是有字体可被应用

下面收集一些包含中文网站的 font-family

知乎:

-apple-system, BlinkMacSystemFont, "Helvetica Neue", "PingFang SC", "Microsoft YaHei", "Source Han Sans SC", "Noto Sans CJK SC", "WenQuanYi Micro Hei", sans-serif;

豆瓣:

Helvetica, Arial, sans-serif;

微博:

Arial, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif;

京东:

tahoma, arial, "Microsoft YaHei", "Hiragino Sans GB", u5b8bu4f53, sans-serif;

V2EX:

"Helvetica Neue", "Luxi Sans", "Segoe UI", "Hiragino Sans GB", "Microsoft Yahei", sans-serif, "Apple Logo";

Airbnb 中国:

Circular, PingFang-SC, "Hiragino Sans GB", 微软雅黑, "Microsoft YaHei", "Heiti SC";

Bilibili:

-apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, Arial, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;

掘金:

-apple-system, system-ui, "Segoe UI", Roboto, Ubuntu, Cantarell, "Noto Sans", sans-serif, BlinkMacSystemFont, "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", Arial;

中文维基百科:

sans-serif;

从上面的一堆 font-family 里,我们可以总结出几个热点字体:

-apple-system, BlinkMacSystemFont: Apple 设备专属,应该是一般加载 SF Pro,有中文的时候加载 PingFang SC

Helvetica Neue:一款发布于 1983 年的英文字体

Microsoft YaHei:微软雅黑,Windows 7 以上系统专属

Hiragino Sans GB:冬青黑体,macOS iOS 早年内置字体

WenQuanYi Micro Hei:文泉驿微米黑,广泛内置于 Linux 类系统

Segoe UI: Windows 7+ 内置英文字体

Roboto:安卓平台泛用字体

Ubuntu:Ubuntu 系统默认字体

Arial:绝大多数操作系统内置,类似 Helvetica Neue

怎么列举出一个足够好的 font-family?

sans-serif. Period.

说实话,我相信绝大多数的操作系统/浏览器都会排列好自己所有的字体,所以绝大多数情况下一个 sans-serif 解千愁。

其次照顾好各大操作系统,且以大致的字体质量排列:

Apple Devices: -apple-system, BlinkMacSystemFont

Windows 7+: Segoe UI, Windows XP: Tahoma

Android: Roboto,Earlier Android: Droid Sans

Ubuntu: Ubuntu

其他泛用英文字体:

Helvetica Neue, Arial

非衬线字体集:sans-serif

中文字体:

Apple Devices: PingFang SC, Hiragino Sans GB

Windows 7+: Microsoft YaHei

Windows XP: HeiTi

Android: Noto Sans CJK, Earlier Android: Droid Sans

Ubuntu: Noto Sans CJK, WenQuanYi Micro Hei

最后抄一下别人的,串起来,加上 Emoji:

en {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Roboto, Oxygen, Cantarell, "Fira Sans", "Liberation Sans", "Droid Sans", Ubuntu, "Helvetica Neue", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}
zh {
font-family: font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Roboto, Oxygen, Cantarell, "Fira Sans", "Liberation Sans", "Droid Sans", Ubuntu, "PingFang SC", "Noto Sans CJK SC", "Noto Sans SC", "Hiragino Sans GB", "WenQuanYi Micro Hei", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}

为什么不用 system-ui

当然,最近各大手机厂商都放出了自家的中文字体,我们也可以加上:

zh {
font-family: font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Roboto, Oxygen, Cantarell, "Fira Sans", "Liberation Sans", "Droid Sans", Ubuntu, "PingFang SC", "HarmonyOS Sans SC", MiSans, "Hiragino Sans GB", "WenQuanYi Micro Hei", "Noto Sans CJK SC", "Noto Sans SC", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}

OPPOSans 因为字重奇怪被除名。

本博客的实践

首先在 font-family 里优先苹方,HarmonyOS Sans,MiSans,冬青黑体和文泉驿微米黑,通过 75CDN 引入 Noto Sans SC 并且 font-display: swap,把微软雅黑仅仅放在 sans-serif 的前面。

坚决做到除非用户断网不然就不能让微软雅黑活下去

此外博客 Logo 是 Noto Sans SC Thin @100,Slogan 是 Noto Serif SC Extralight @200,均直接做成子集然后 inline-css。

引用

除了文章中提及的外部资源,本文还参考了以下文章,顺序不分先后:


感谢阅读。第一次写这么长的文章,我相信一定有错漏、描述不正确的地方,欢迎在评论区予以指正。

如需全文转载本文,请联系我获取免费授权;除非获得授权,本文禁止全文转载。