先理清几个概念
随着矢量字体的出现,尺寸的概念逐渐模糊,『字型』和『字体』、Font 和 Typeface 也不再被那么严格区分,基本被当做一个意思。
字体的构成主要包括轮廓格式、封装格式、编码方式三方面。
重点在于数据格式,主要有:
除了一些特殊设备,目前普遍使用的是轮廓字体,主要有以下几种:
从 CSS1 开始就定义了字体匹配算法, CSS2 Fonts matching algorithm 、CSS Fonts Module Level 3 和 CSS Fonts Module Level 4 也先后做了一些修改演变。
但总体思路不变,要求 UA 先创建字体属性对应的数据库,然后对每个字符尝试匹配第一个 font-family
名字,再尝试匹配其余 font 属性。family name
的匹配也陆续支持匹配一组字体(非某个特定字体),支持本地化名称匹配。
而一旦找不到,就需要使用 Fallback 字体作为兜底。
不同的操作系统和浏览器搭配,会提供不同的默认字体。 具体到网页来说,影响页面默认 fallback 字体的因素包括:系统内置、浏览器配置、charset、lang 属性、font-family 中之前项的值等。 比如 windows 的相关机制:
css 中可以用 system-ui
来选择当前操作系统的默认字体,这样可以让 web 页面的文字视觉风格跟原生 App 一致。
在兼容性方面,除了 IE,最近的主流浏览器都支持 system-ui
,OSX 10.11 在之前也支持-apple-system
的兼容写法。
由于无衬线字体更适合屏幕阅读,所以各个系统一般都会带有无衬线字体 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,默认西文字体也可能会因为厂商定制而不一样。
思源系列字体,是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
:
.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 提供的字体托管服务,那么也就免不了要对加载速度做一系列优化。 相关最佳实践包括四步:
<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 的生命周期:
浏览器默认会采用 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.com
、 fonts.googleapis.com
两个域名被阻断的概率还是太高,连不上,更遑论加载优化。
因此,我最终还是换成了本地化字体。Google WebFont Helper 支持我们按需配置并下载 WebFont 字体。
为了减少无用字符,我用的几种字体中,『思源宋体』(Noto Serif SC) 和『思源黑体』(Noto Sans SC) 只选择简体中文字符,其余只选择拉丁字符。
而在 @font-face css
方面,我同样加上了 font-display: swap
, 由于我只考虑支持现代浏览器,所以只选择了 woff
和 woff2
格式。
@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 :
<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 基本需要把他们都配上:
尽管我把字体本地化了,但如果完全 self-host 还是开销太大,依然还需要通过 cdn 加速。我最终选择的是 360 奇舞团维护的静态资源 75 CDN,它竟然令人惊喜的提供了 Google Font 本地化,输入 https://fonts.googleapis.com/css2?family=Merriweather:300
,
会直接返回相应的本地化资源 :
/* 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) 依然自行托管。
最后总结下我的实践:
font-display: swap
优化加载。