中文网页字体设置方案:从 font-family 到 font-display

从13年刚开始搭建博客的时候我就在关注中文的 webfont 解决方案了,可惜那时候几乎找不到一套成熟的跨平台字体设置方案,Google Fonts 和 Typekit 也远没有现在这么完善,Windows 下微软雅黑丑出天际,我们能做的无非就是老老实实设置好 fallback 来确保不同操作系统下都能显示出我们希望显示的字体。

到了2019年就不一样啦,现在的 Google Fonts 和 Adobe Typekit 都提供了傻瓜式的第三方字体托管,同时 CDN 的普及也确保了不同地域的用户都能够(比较)迅速地加载非本地字体。更关键的是,今天即使是移动设备的带宽也允许用户在1–2秒内完成一套中文字体的加载。我们已经完全有理由抛弃微软雅黑而采用更灵活的中文网页字体设置方案。

font-family

font-family 的填写原则在网络上可以搜出无数文章,这里我仅仅简单说一下我自己的一些总结:

  • 英文字体在前,中文字体在后。原因很简单,中文字体包中通常包含了英文字符,反之则不成立,如果先设置了中文字体,那么英文字体根本轮不到 fallback
  • 操作系统方面,Apple 系列(macOS + iOS)字体在前(直接用 -apple-system 即可,见下文),Windows 字体在后。这里解释了原因,即,macOS 下的用户有一定概率安装了 Windows 字体(因为安装了 Office),而 Windows 下安装 macOS 字体的用户却少之又少,所以如果 Windows 在前,那么在 macOS 下很有可能就出现「放着苹方不用却用微软雅黑」这种坑爹情况
  • 根据访问网站的用户群体不同,可以考虑放弃对于老旧操作系统和浏览器的支持,比如我在使用思源宋体的时候,仅提供了 .woff 和 .woff2 格式的字体文件,完全放弃了老版本的 IE 和 Safari
  • 仅对 Windows 用户使用自定义字体,比如思源系列,macOS、iOS、Linux 就使用系统自带字体就好。原因嘛当然是因为 Windows 自带的中文字体实在找不到好看的,而主动安装字体的用户毕竟还是少数(嗯这条是私货 🙄 )
  • 衬线还是非衬线完全看个人审美喜好,比如我就不觉得思源黑体系列好看(所以你现在看到的思源宋体,如果你是 Windows 用户的话)。同时,中文衬线搭配英文非衬线,或者反过来,中文非衬线搭配英文衬线,也完全没有问题

我目前使用两套 font-family 设置。正文(Main text)部分是:

font-family: Georgia, -apple-system, 'Nimbus Roman No9 L', 'PingFang SC', 'Hiragino Sans GB', 'Noto Serif SC', 'Microsoft Yahei', 'WenQuanYi Micro Hei', 'ST Heiti', sans-serif

标题(Headings)和页面导航栏(Navigation)部分是:

font-family: 'Josefin Sans', -apple-system, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft Yahei', 'WenQuanYi Micro Hei', 'ST Heiti', sans-serif

其中 Georgia-apple-system'Nimbus Roman No9 L' 分别对应 Windows、macOS/iOS、Linux 下三种系统内置英文字体(其实 Georgia 就已经覆盖三种系统了,后面两个只是出于保险起见),'PingFang SC'(苹方)、'Hiragino Sans GB'(冬青黑体)、'Microsoft Yahei'(微软雅黑)对应三种系统内置中文字体(按照上文说法,Apple 系列在前,Windows 在最后),'Josefin Sans''Noto Serif SC'(思源宋体)则是两套允许免费使用的第三方字体,已经被我存储在本地。

理论上来说,一旦在 font-family 中放置了类似思源宋体这种第三方字体,那么浏览器就一定会加载该字体文件,因此也就没有必要再考虑后续的 fallback 字体。但是就像在 font-display 里面说到的,为了避免浏览器在完成加载第三方字体文件之前显示一片空白,我们仍然有必要设置 fallback 字体作为备胎。

根据 Google 的标准,如果在第三方字体完成加载之前页面没有显示出任何文字,那么在 PageSpeed Insights 等网页测试工具中都是要扣分的。

Google Fonts 的本地化

在 Google Fonts 的官方仓库中,中文字体包通常会被拆分为几十上百不等的若干个字体文件,从而确保打开某个网页时仅加载该页面中需要的字符。这种思路和 font-spider 这类的字体压缩工具类似,但是把加载哪些字体文件完全交给 CSS 判断,因此压缩比自然远不能与后者相比。除此之外,还有下下几点因素让我决定放弃使用 Google Fonts 进行字体在线托管,而选择将其部署在本地:

  • font-weight: 400 的思源宋体为例(Google Fonts 里的名字叫做 Noto Serif SC),对于单个阅读时间在3分钟左右的中文页面,需要加载18–22个左右的 .woff2 字体文件,总的体积在 900KB–1.1MB 左右。而如果选择将完整的字体包存储在本地的话,也仅需要 2MB 不到的空间而已。在我看来,用请求数的大幅增加(20 vs 1)来换取体积的减少(1MB vs 2MB)似乎并不怎么划算
  • 将字体数据分散在20来个字体文件中,并为每个文件都设置 font-display: swap 属性(Google Fonts 的默认策略,后文会解释),会导致整个页面由于字体的切换而产生一种不断闪烁的错觉,我个人无法接受这种情况
  • 虽然 gstatic.com 目前没有被墙(也有一说是直接被解析到国内服务器上了),但是我总觉得还是本地存储比较保险一些,然后再使用 CDN 来保证加载速度

要把 Google Fonts 中的第三方字体部署在本地也再容易不过了,只需要在 google-webfonts-helper 中选择想要的字体进行下载并存储在本地 FTP 指定目录下,然后把其提供的 CSS 代码添加到网页原有的样式后。以 WordPress 为例,可以把这段 CSS 代码复制到主题对应的 style.css 中,或者也可以新建一个 CSS 文件然后利用 wp_enqueue_stylewp_register_style 函数使得这个 CSS 文件可以被 WordPress 访问到。由于我使用的主题已经内置了 Google Fonts 的功能,所以我直接在 functions.php 中使用了一个同名函数覆盖掉了原有的从 fonts.googleapis.com/css 获取样式的函数:

function generate_google_fonts_link() {
    // replace remote stylesheet in fonts.googleapi.com with 
    // the local file
    return '/assets/google-fonts.css'; 
}

function load_google_font() {
if ( $fonts_link = generate_google_fonts_link() ) {
    wp_enqueue_style( 'google-fonts', $fonts_link, false );
}

add_action( 'wp_enqueue_scripts', 'load_google_font' );

其中的 /assets/google-fonts.css 就是 .woff 和 .woff2 字体文件所在的相对路径。

我的 google-fonts.css 的具体内容为:

/* josefin-sans-regular - latin_latin-ext_vietnamese */
@font-face {
  font-family: 'Josefin Sans';
  font-style: normal;
  font-weight: 400;
  src: local('Josefin Sans Regular'), local('JosefinSans-Regular'),
       url('/assets/fonts/josefin-sans-v14-latin_latin-ext_vietnamese-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
       url('/assets/fonts/josefin-sans-v14-latin_latin-ext_vietnamese-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
  font-display: swap;
}

/* noto-serif-sc-300 - chinese-simplified_latin */
@font-face {
  font-family: 'Noto Serif SC';
  font-style: normal;
  font-weight: 300;
  src: local('Noto Serif SC Light'), local('NotoSerifSC-Light'),
       url('/assets/fonts/noto-serif-sc-v6-chinese-simplified_latin-300.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
       url('/assets/fonts/noto-serif-sc-v6-chinese-simplified_latin-300.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
  font-display: swap;
}

/* noto-serif-sc-900 - chinese-simplified_latin */
@font-face {
  font-family: 'Noto Serif SC';
  font-style: normal;
  font-weight: 900;
  src: local('Noto Serif SC Black'), local('NotoSerifSC-Black'),
       url('/assets/fonts/noto-serif-sc-v6-chinese-simplified_latin-900.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
       url('/assets/fonts/noto-serif-sc-v6-chinese-simplified_latin-900.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
  font-display: swap;
}

font-display

如果你仔细一点看上面的 CSS 代码的话就会发现,不同于从 google-webfonts-helper 中直接复制出来的代码,我还为每个字体指定了一个 font-display 字段。font-display 是一个比较新的 CSS 属性,用来控制某一字体在尚未成功加载时采用何种方式显示文本。如果不明确指定 font-display,浏览器一般会采用「block」的方式进行显示,即,在该字体完全加载成功之前啥都不显示。

对于英文字体来说即使使用 block 问题也不大,因为哪怕是非本地的字体文件也就几十 KB,完全可以在几百甚至几十毫秒内完成加载,对用户几乎不会造成任何影响。但是对于中文字体来说这个问题就比较严重了(特别是对于我这种使用单个大体积 .woff2 文件的情况),因为在完成字体加载之前,通常会有一段肉眼可见的页面空白时间,对用户非常不用好:

font-display 默认为 block,会导致字体加载完成(第 650ms 左右)之前页面一片空白。加粗的字体较早显示出来说明 noto-serif-sc-900.woff2 文件早于 noto-serif-sc-300.woff2 加载完毕。左上角时间为真实的页面加载时间,动画为 x7 慢放,单击可查看大图

为了改善这个情况,我们在使用第三方中文字体时,非常有必要把 font-display 设置为 swap 或者 fallback,前者会在当前字体文件加载时使用 font-family 中当前字体的后备字体进行文本显示,而后者则允许一个停顿时间(比如100ms),该停顿时间内先显示空白,如果过了这个时间阈值当前字体仍然没有完成加载,则采用和 swap 相同的策略进行文本显示。比如我们在 CSS 中对正文文本使用 font-family: 'Noto Serif SC', 'Microsoft Yahei', sans-serif,那么当思源宋体的字体文件还没加载完成时,浏览器会先用微软雅黑显示当前的正文文本(本机中找不到微软雅黑的话继续 fallback 到系统默认的非衬线字体),待思源宋体加载完毕后再立即切换过去:

font-display 换成 swap 后的效果。思源宋体的字体文件在第 550ms 左右下载完成,在这之前使用微软雅黑作为 fallback 进行显示,然后立刻切换为思源宋体。左上角时间为真实的页面加载时间,动画为 x7 慢放,单击可查看大图

除了 blockfallbackswap 之外,font-display 还有 optional 这个选项,即让浏览器自己做决定是否要对字体进行切换,对于一些网速比较慢的网页,第三方字体加载完成可能需要花费很长时间,这种情况下即使字体文件已经完成了加载浏览器也可能不再进行切换,而是继续以 fallback 字体进行显示。


最后总结一下我认为的目前可接受的中文 webfont 解决方案:

  1. 字体文件在本地存储,用 CDN 进行加速
  2. 合理设置 font-family,做好 fallback
  3. 使用 font-display: swap 属性(optional 也可)

Reference

About the author

Jueqin

本作品以 CC BY-NC-ND 许可协议进行发布。

如果您认为文章对您有用的话,不妨请我喝一杯咖啡?

Add comment