字体漫谈-网站字体最佳实践

字体基础

先理清几个概念

  • 字符 Character:数字、字母、符号等, 一个图形实体。
  • 字符集 Character set:字符的集合,但在风格形状上不一定统一。
  • 字形 Glyph:特指某个字符的形状,可辨认的图形实体,同一个字符可以有很多字形。
  • 字型 Font:源于铅字印刷,指一整套具有相同设计的字形集合,计算机领域的字型 Font 就特指某个字体文件(字库),比如一套黑体五号字。
  • 字体 Typeface:当一系列风格统一的字型在一起,就形成体的概念,可以用字体来描述,比如 宋体。

随着矢量字体的出现,尺寸的概念逐渐模糊,『字型』和『字体』、Font 和 Typeface 也不再被那么严格区分,基本被当做一个意思。

字体分类

衬线

  • 无衬线 sans serif:无衬线适合屏幕阅读、适用于科技类文章。
  • 衬线 serif:衬线字体更为优雅隽秀,接近传统纸张阅读的感觉,印刷效果漂亮,适用于文学性的文本。

字符距离

  • 比例字体 proportional,适合显示普通文本。
  • 等宽字体 monospace,字符和标点排列工整,宽度相同,经常会对个别字母和符号做显示优化,适合展示代码。比如 『FiraCode』,『Input mono』等等。

字体构成

字体的构成主要包括轮廓格式、封装格式、编码方式三方面。

  • 数据格式,用来描述字形。
  • 编码方式,决定字符编号,字形对应关系。
  • 封装格式,决定字体文件大小格式

重点在于数据格式,主要有:

  • 点阵字体 bitmap(位图字体),分辨率低、不适合放大,渲染快,适用于低像素输出设备。
  • 轮廓字体,通过贝塞尔曲线描述字形,向量图集合,适合缩放。
  • 笔画字体。

除了一些特殊设备,目前普遍使用的是轮廓字体,主要有以下几种:

  • PostScript(PS) 轮廓字体,Adobe 开发的,用三次贝塞尔曲线描述字形,效果好适合印刷打印,根据封装格式和编码方式不同又有 Type1、CID、Type0 等类型。
  • TrueType(TT),苹果为对抗 Adobe,与微软共同开发,用二次贝塞尔曲线描述字形,渲染快,OSX 和 Windows 上最常见的字体。
  • OpenType,微软先独自开发目标对抗 true type,之后 Adobe 加入,是 TrueType 的升级兼容版,也增加了对PostScript 轮廓的支持,常见有 OpenType layout & PostScript Outlines。

Web 字体

字体匹配

从 CSS1 开始就定义了字体匹配算法, CSS2 Fonts matching algorithmCSS Fonts Module Level 3CSS Fonts Module Level 4 也先后做了一些修改演变。

但总体思路不变,要求 UA 先创建字体属性对应的数据库,然后对每个字符尝试匹配第一个 font-family 名字,再尝试匹配其余 font 属性。family name 的匹配也陆续支持匹配一组字体(非某个特定字体),支持本地化名称匹配。

而一旦找不到,就需要使用 Fallback 字体作为兜底。

Fallback 字体

不同的操作系统和浏览器搭配,会提供不同的默认字体。 具体到网页来说,影响页面默认 fallback 字体的因素包括:系统内置、浏览器配置、charset、lang 属性、font-family 中之前项的值等。 比如 windows 的相关机制:

系统字体

css 中可以用 system-ui 来选择当前操作系统的默认字体,这样可以让 web 页面的文字视觉风格跟原生 App 一致。 在兼容性方面,除了 IE,最近的主流浏览器都支持 system-ui ,OSX 10.11 在之前也支持-apple-system的兼容写法。

systemui-caniuse

由于无衬线字体更适合屏幕阅读,所以各个系统一般都会带有无衬线字体 sans-serif 并做为最后兜底。

OSX 上默认西文字体早期有 Helvetica Neue ,目前是 San Francisco 。Windows 上尽管 system-ui 有兼容性问题,但默认西文字体还是有的,比如早期的 Microsoft Sans Serif,目前的 Segoe UI。Andorid 上是 Roboto。

但对于中文字体呢?如果不设置中文字体,显示会有很多不同。 比如Windows 上无论是否衬线,早期 汉字默认最终都会落到『宋体』(中易宋体),现在一般是『微软雅黑』 。苹果方面,早期 serif 为 「Times」,汉字会使用「华文宋体」,sans-serif 为「Helvetica」,汉字会 使用「华文黑体」(STHeiti),在 OSX 10.11 & IOS 9 之前推出过『冬青黑体』,之后中文则是 『苹方』。 Andorid 使用的就是 Google 自家的 『思源黑体』。

浏览器字体

对于西文字体,目前的 Chrome 在没有配置 generic family 的时候会选择 Arial,注意这是无衬线字体。 Chrome 会选择系统的默认中文字体,即 Windows 上『微软雅黑』,OSX 上『苹方』

Safari 的默认中文字体是 『苹方』,西文字体 是 『Times New Roman』

早期 GBK 编码下,Window 和 IE 会错误选择中文默认字体。现在绝大多数网站都会选用 UTF-8 编码并指定 lang 为 zh-CN。在指定了 lang 为 zh-CN后 Safari 对中西文的默认字体都会指向『苹方』

Android 上的浏览器则比较混乱,默认西文字体是 『Roboto』,中文字体『思源黑体』,但也会有厂商自定义默认字体,同时即便采用 UTF-8 编码并指定 lang 为 zh-CN,默认西文字体也可能会因为厂商定制而不一样。

自定义 Web font

思源 Pan-CJK 系列

思源系列字体,是Adobe 和 Google 合作开发,对日文、韩文、繁体中文和简体中文,7 种粗细类型都做了支持优化,不但免费而且开源的 OpenType 字体和源文件,我们都可以基于源码修改后个人或商业使用。

Pan-CJK 系列最早 在2014 年 首发,The Typekit Blog | 隆重介绍 思源黑体:一款Pan-CJK 开源字体 Adobe 称为「 思源黑体 」(Source Han Sans,源ノ角ゴシック、본고딕),Google font 中的名称是 Noto

2017 年发布了 思源宋体 (Source Han Serif,源ノ明朝、본명조)。

顾名思义,思源黑体和思源宋体分别属于无衬线和衬线字体。

个人字体选型

使用三套字体

对于个人网站,还是需要显式声明 font-family,尽量避免让系统和浏览器选择字体。

通常我把西文字体放在中文前,先别让中文字体渲染英文字符,再让中文字符 fallback 到后面我期望的中文字体。同时还要考虑下系统版本,最后再列出 generic family 兜底。

另一方面,我希望区分衬线,对与技术类文章使用无衬线 sans serif 字体,对不太硬核的文章(尤其中文)使用衬线 serif 字体。

在首页、介绍等页面上,内容以无衬线字体方便快速阅读,但对标题用衬线字体强调。

最后,对于代码块,在 HTML 中的 pre、code 标签就比较适合使用等宽字体。

因此我定义了三套 font-family:

CSS
.serif() {
  font-family: 
    'Merriweather',
    'Noto Serif SC',
    serif;
}
.sans-serif() {
  font-family:
    system-ui,
    -apple-system,
    Arial,
    'Noto Serif SC',
    'Microsoft YaHei', 
    sans-serif;
}
.monospace() {
  font-family: 
    'Fira Code',
    system-ui,
    -apple-system,
    Arial,
    sans-serif;
}

字体加载处理

Google Font 托管最佳实践

之前一直用 Google Font 提供的字体托管服务,那么也就免不了要对加载速度做一系列优化。 相关最佳实践包括四步:

  • 预连接字体源,给字体源预热
  • 预加载字体 css,告诉浏览器作为字体下载,调整优先级
  • 异步加载字体 css 兜底,浏览器会以低优先级处理异步 css 请求,从而不阻塞页面解析,并且仅应用于 print media
  • noscript 再兜底用户禁用 js 的场景
HTML, XML
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Fira+Mono&family=Merriweather:wght@300;400;700&family=Noto+Sans+SC:wght@300;400;700&family=Noto+Serif+SC:wght@300;400;600;700&family=Open+Sans:wght@300;400;600;700&display=swap" as="font" crossorigin />
  <link href="https://fonts.googleapis.com/css2?family=Fira+Mono&family=Merriweather:wght@300;400;700&family=Noto+Sans+SC:wght@300;400;700&family=Noto+Serif+SC:wght@300;400;600;700&family=Open+Sans:wght@300;400;600;700&display=swap" rel="stylesheet"
      as="font" 
      crossorigin
      media="print" onload="this.media='all'" />
  <noscript>
    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Fira+Mono&family=Merriweather:wght@300;400;700&family=Noto+Sans+SC:wght@300;400;700&family=Noto+Serif+SC:wght@300;400;600;700&family=Open+Sans:wght@300;400;600;700&display=swap" crossorigin />
  </noscript>

同时,Google Font 生成的字体样式 url 现在也都会带上 font-display 属性

Web Font 生命周期

这其实涉及到 Web Font 的生命周期:

  • block 期,浏览器以不可见字体,渲染文本。
  • swap 期,浏览器以 fallback 字体渲染文本。
  • failure 期,浏览器找不到目标字体,永远以 fallback 字体渲染。

浏览器默认会采用 font-display: auto, 实际效果类似 font-display: block 的方式渲染,在目标字体加载完成前先渲染成不可见文本,在目标字体加载完成后立即切换过去。所以表现出的效果就是在字体加载完成之前不显示,这也是引入三方字体很容易导致白屏 FOIT(Flash of Invisible Text) 瞬时不可见文本的重要原因之一。

如果配置成 font-display: fallback,浏览器会先进入 block 期渲染不可见文本,这个 block 期很短(表现出短暂的页面白屏停顿,即所谓 FOUT(Flash of Unstyled Text) 瞬时未样式化文本效果);block 期过后进入 swap 期,如果目标字体还没加载完成,尝试用 fallback 字体渲染,一但目标字体加载,切换过去;swap 期很短,如果长时间目标字体加载未果,就会一直使用 fallback 字体。

如果配置成 font-display: swap,浏览器会跳过 block 期(但还是会有微小的等待,接近 0 秒),如果目标字体还没加载完成,直接用 fallback 字体渲染,并且 swap 期无限长,一直等待到目标字体加载,切换过去。

Google Font 生成的字体样式 url 都会设成 font-display: swap,个人认为也是比较好的实践方式。

但!

我最后还是放弃了 Google Font。一方面是因为 Google Font 会把中文字体拆成多个『字体切片』来保证对页面的按需加载。比如我的网站使用的字体会是 17 个 .woff2 文件请求,总体积在 700KB 左右,但这些字体即便不做按需加载总共也不过 1 MB,在如今的网络条件下这种优化效果意义并不大,而 17 个配置了 font-display: swap 的分散字体,还会出现页面因为陆续切换字体的内容错位效果;另一方面,无论如何优化加载速度,国内 gstatic.comfonts.googleapis.com 两个域名被阻断的概率还是太高,连不上,更遑论加载优化。

Google Font 本地化

因此,我最终还是换成了本地化字体。Google WebFont Helper 支持我们按需配置并下载 WebFont 字体。

为了减少无用字符,我用的几种字体中,『思源宋体』(Noto Serif SC) 和『思源黑体』(Noto Sans SC) 只选择简体中文字符,其余只选择拉丁字符。

而在 @font-face css 方面,我同样加上了 font-display: swap, 由于我只考虑支持现代浏览器,所以只选择了 woffwoff2 格式。

  • woff(web open font format), Mozilla 联合各类组织专为 web 设计,对字体做了压缩,相比 tff 格式体积大减,加载速度更快。
  • woff2, woff 的下个版本,有更好的压缩率。
CSS
@font-face {
  font-family: 'Noto Serif SC';
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: local('Noto Serif SC'),
       url('../fonts/noto-serif-sc-v22-chinese-simplified-300.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
       url('../fonts/noto-serif-sc-v22-chinese-simplified-300.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}

注意我还设置了本地字体资源 local() ,由于字体文件通常不小,这可以告诉浏览器优先寻找本地机器上的字体资源。

配合字体加载 css :

HTML, XML
  <link rel="preload" as="font" type="font/woff2" href="/font/noto-sans-sc.css" >
  <link rel="stylesheet"  type="text/css" href="/font/noto-sans-sc.css"  media="print" onload="this.media='all'" />

其他字体格式也备注下,想要各种浏览器都支持 web font 基本需要把他们都配上:

  • svg,适量字体,legency ios(ios 4 之前) safar 唯一支持的字体格式,占内存少。
  • ttf(TrueType Font),前面提过,OSX 和 Windows 上最常见的字体,但没有压缩,资源文件较大。
  • otf(OpenType Font),Windows 和 IE 常用,没有压缩,资源文件较大。
  • eot(Embedded OpenType),微软开发,早期 IE 独有字体格式。

CDN 加速托管本地化字体

尽管我把字体本地化了,但如果完全 self-host 还是开销太大,依然还需要通过 cdn 加速。我最终选择的是 360 奇舞团维护的静态资源 75 CDN,它竟然令人惊喜的提供了 Google Font 本地化,输入 https://fonts.googleapis.com/css2?family=Merriweather:300 , 会直接返回相应的本地化资源 :

CSS
/* merriweather-300 */
@font-face {
  font-family: 'Merriweather';
  font-style: normal;
  font-weight: 300;
  src: url('//lib.baomitu.com/fonts/merriweather/merriweather-300.eot'); /* IE9 Compat Modes */
  src: local('Merriweather Light'), local('Merriweather'),
       url('//lib.baomitu.com/fonts/merriweather/merriweather-300.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
       url('//lib.baomitu.com/fonts/merriweather/merriweather-300.woff2') format('woff2'), /* Super Modern Browsers */
       url('//lib.baomitu.com/fonts/merriweather/merriweather-300.woff') format('woff'), /* Modern Browsers */
       url('//lib.baomitu.com/fonts/merriweather/merriweather-300.ttf') format('truetype'), /* Safari, Android, iOS */
       url('//lib.baomitu.com/fonts/merriweather/merriweather-300.svg#Merriweather') format('svg'); /* Legacy iOS */
}
  

不过遗憾的是一些新字体这个服务还没同步,比如 『思源宋体-简体中文』(Noto Serif SC),所以我前面通过 [Google WebFont Helper 下载字体的操作也不算白忙活,对『思源宋体-简体中文』(Noto Serif SC) 依然自行托管。

总结

最后总结下我的实践:

  • 区分衬线,技术类文章使用无衬线 sans serif 字体,不太硬核的文章使用衬线 serif 字体,改善中文阅读体验。
  • 在首页、介绍等页面上以无衬线字体方便快速阅读,但对标题用衬线字体修饰。
  • 对于代码块用等宽字体。
  • 字体资源尽量本地化并 CDN 加速。
  • 设置 font-display: swap 优化加载。

引用