Web Fonts 的优化:Web Fonts 字体加载策略

发布于 大漠

字体加载优化策略

前面的篇幅告诉我们,使用 Web Fonts 会造成布局偏移,页面渲染时会发生重排和重绘。这会让页面在渲染时变得更慢,用户体验会更差。如果我们要对此进行优化,减少 Web Fonts 引起的布局偏移,就要从字体加载方面去做相应的优化策略。@Zach Leatherman 经过多年的研究,对字体加载提供了一些优化策略

使用 font-display 来防止布局徧移

font-displayCSS Fonts Module Level 4 中规范中的一部分,主要用来告诉浏览器在不同的下载时间和可用时间下是如何渲染文本的。

不管是 FOIT 还是 FOUT (字体加载策略),两者都不太理想,会产生布局偏移。庆幸的是,在 @font-face 规则中添加 CSS 的 font-display 可以告诉浏览器我们更喜欢 Web Fonts 在不同的下载时间和可用时间下以哪种方式来渲染文本。

@font-face {
    font-family: AlibabaSans102;
    font-display: optional;
    src:
        url('/font/AlibabaSans102.woff2') format('woff2'),
        url('/font/AlibabaSans102.woff') format('woff'),
        url('/font/AlibabaSans102.ttf') format('truetype');
}

font-display 有五个可选值(autoswapblockfallbackoptional),其中 auto 是它的默认值,也就是浏览器渲染 Web Fonts的默认行为(大多数浏览器喜欢 FOIT)。另外四个值在字体未加载完的前后,浏览器将会以不同的形式(Web Fonts 还是备用字体)渲染文本,即 修改 Web Font 的渲染行为。在开始介绍了解这几种类型之前先来了解一个基本概念:字体显示时间轴

字体显示时间轴

字体显示时间线基于一个计时器,该计算器在用户代理(浏览器)尝试使用给定下载字体的那一刻开始。时间线分为三个时间段,在这三个时间段中指定使用字体的元素的渲染行为。

  • 字体阻塞周期(Block):如果未加载字体,任何试图使用它的元素都必须渲染不可见的后备字体。如果在此期间字体已成功加载,则正常使用它
  • 字体交换周期(Swap):在阻塞周期后立即发生,如果未加载字体,任何尝试使用它的元素都必须渲染后备字体。如果在此期间字体已成功加载,则正常使用它
  • 字体失败周期(Fail):在交换周期后立即发生,如果在此周期开始时字体还未加载,则标记为加载失败,使用正常的后备字体。否则,字体就会正常使用

有了这个概念,我们开始来了解 font-displayswapblockfallbackoptional 会让浏览器以什么方式(字体显示时间轴)来渲染使用了 Web Fonts的文本。先从 swap 开始!

swap

font-display 取值为 swap 会告诉浏览器,Web Font在未加载完成之前都采用备用字体来显示文本(也就是 FOUT)。不管 Web Font 加载费时多久,只要字体被加载,文本就会从备用字体切换到 Web Font。使用 swap 方式的优势是可以让用户立即看到内容,但备用字体最好是能和 Web Font 相似,以防止字体交换(备用字体切换到 Web Font)时出现较大的布局偏移。

block

font-display 取值 block 则会告诉浏览器,Web Font 在未加裁之前隐藏文本(也就是 FOIT)。不过 block 并不会让使用 Web Font 的文本永远隐藏不可见:如果 Web Font 在一定时间内(通常是 3s)未加载,文本不可见,但超过3s这个时间,浏览器会使用备用字体渲染文本(介于 Web Font 加载时间大于 3s 与加载完成之间),一旦 Web Font 加载成功,就会从备用字体切换到 Web Font。

是否选择 block 就得看你自己的选择了。如果你说找不到和 Web Font 相似的备用字体,又不希望在字体切换时造成较大的布局偏移,你可以选择 block ,但你也得记住,使用 block 时有可能会有近 3s 的时间内用户什么也看不到(在这个时间段内文本被隐藏)。这有可能会让用户感觉访问的页面什么内容都没有,甚至会觉得加载页面失败。

fallback

font-displayfallbackswap 很相似,但有两点不同:

  • 字体阻塞期非常的小,大约 100ms ,如上图所示,Web Font在小于 100ms 未加载完,文本会不可见,一旦超过 100ms 就会使用备用字体,使用备用字体渲染
  • 如果 Web Fonts 在 3s 内没有加载,会一直使用备用字体渲染

即 Web Fonts 在小于 0.1s 未加载时,文本不可见(文本有0.1s 不可见期); 0.1s ~ 3s 内加载成功,则会在 0.1s ~ 3s 内使用备用字体渲染,字体一旦加载完就会切换到 Web Fonts 渲染,3s 内还未加载成功(即使在超过 3s 字体加载成功,比如 4s 时加载完成),文本也会一直使用备用字体渲染,好像是字体没加载成功一样。

如果你并不关心用户在第一次访问你的 Web 应用时是否看到你的 Web Fonts(很可能他们自己也不那么关心),那么 fallback 是一个不错的选择。

optional

optionalfallback 类似,但它给字体一个极短的时间(~100ms)来加载,之后就不会被切换。从上图的字体显示时间轴上可以看出,如果 Web Fonts 在小于 0.1s 内未加载完成,即使在后面字体完成加载,文本也不会使用 Web Fonts 渲染。同样,它也有一个极短时间(~100ms)会让文本不可见。

不过,它确实有一个额外的功能,即如果连接速度太慢,字体无法加载,它可以让浏览器中止字体的请求。

optional 对 Web 可用性有明显的好处,而且它对慢速的网络连接的数据占用也有改善。不过 optional 也有着自己独特之处。这样说吧,如果显示的是备用字体,那么 Web Fonts 将永远不会被显示,即使是快速的加载(除非在小于 100ms 字体加载完成)。因此,这就导致用户在快速的设备和连接上,文本使用备用字体渲染出来了,Web Fonts 已被加载,但它并没有被渲染出来。只有当用户浏览到另一个页面时,Web Fonts 才会被显示出来。

测试结果

上面是 font-display 属性几个值的使用规范。我们来看他们在 WebPageTest 中的表现。以 Telegraph 为例,连接速度提高到下载和上传为 .6Mbit/s ,而且 Web Fonts 资源是同源情况之下:

我们取几个重要数据:FP(First Paint,首次渲染)时间、带有不可见的占位符文本的页面布局时间、LCP(Largest Contentful Paint,最大内容的绘制)时间,Web Font的交换时间、差异(第一个交换的文本):

  • swap:页面加载的 8.1s 时绘制出了第一个像素,200ms 后,页面结构完成,备用字体被渲染;Web Fonts 最终在 3.8s 后被交换。文字是可阅读的
  • block:页面加载的 7.9s 后绘制出了第一个像素,200ms 后,页面结构看起来完整,但没有文字,直到 3s 后,文字才渲染出来;然后在 4.1s 后,Web Fonts 被换成了备用字体
  • fallback:页面加载的 8.1s 时将第一个像素绘制到屏幕上,大约 100ms 后,页面结构完成,但没有文字被渲染;大约在 200ms 后,备用字体渲染出来,因为 Web Fonts 还需要 3s 多的时间来加载,所以字体交互并没有发生(因为它已经超过了交换的截止点)。从用户角度来说,页面很稳定,可以阅读
  • optionaloptional 是最简单的检查结果之一。页面在加载的 7.7s 时绘制出了第一个像素,100ms 后,页面结构完成,备用字体被渲染。尽管 Web Fonts 正在后台下载,但在这个页面的生命周期内,它永远不会被显示。如果用户浏览到网站上另一个使用相同字体的页面,这时他们就会看到这个字体(因为,它现在存在于浏览器缓存中),但对于当前页面来说,这个字体不会被使用

简单小结一下

CSS 的 font-display 允许我们使用 auto (浏览器默认行为)、swapblockfallbackoptional 等值修改浏览器对 Web Fonts 的渲染行为。在加载 Web Fonts 时,我们要防止布局偏移(可以使用 CWV 的 CLS 来计算 Web Fonts 引起的布局偏移)。这发生在两种情况之下:

  • FOUT :备用字体被换成了Web Fonts ,例如 font-display: swap
  • FOIT:文本不可见,直到 Web Fonts 加载成功被渲染,例如 font-display: block

浏览器目前有一个类似 block 的默认策略。不过,唯一能消除布局偏移的是 optional ,在结合字体其他加载策略下,optional 将是你最佳选择!

你的 Web 应用上的每一种字体都会有自己的 FOIT 或 FOUT —— 字体在加载时被单独交换(Swap),而不是在它们全部加载完成时交换。这可能会导致一些其他的问题,详见 @Mitt Romney 的 《Web Font Problem》。为了完全控制字体的加载,还需要借助 JavaScript 脚本或其他的一些字体加载策略!

加载更少的字体文件

加载一两个字体文件来渲染文本并不会(可能不会)对页面渲染速度产生巨大影响,但下载多个(比如五个,十个)字体文件对页面渲染速度会(较大可能)产生巨大影响!比如下面所示三个不同网站使用的 Web Fonts,以及页面加载字体时的瀑布流图: ​

从图上也可以略知,字体文件加载数量对页面渲染速度的影响吧!因此,我们应该在满足设计所需,确保你提供最小数量的字体文件,这也是确保浏览器在布局时有这些文件可用的最好方法,从而减少出现 FOUT 布局变化的可能性。让我们来看看在保持设计的同时加载更少字体文件的一些技术。

使用伪粗体和伪斜体

在 Web 中使用 伪粗体 (Faux Bold) 或 伪斜体 (Faux Italic) 可以会引起 FOFT (Flash Of Faux Text),即 **伪文本闪现!**设计师也很讨厌这种效果!

那么什么是伪粗体和伪斜体呢?很多时候,同一个字体通常会包括一堆不同的文件,比如下图中的 Avenir Next 字体就有十二种:

往往同一字体将九种字重(font-weight 的值可以是 100200300400500600700800900 )与常规体和斜体的变体结合起来,就会产生 18 个独立的字体文件!

为此,你可能会使用 @font-face 加载很多个字体文件:

@font-face {
    font-family: 'Lora';
    src: 
        url('/font/Lora-Regular-webfont.woff2') format('woff2'),
    url('/font/Lora-Regular-webfont.woff') format('woff'),
    url('/font/Lora-Regular-webfont.ttf') format('truetype');
    font-weight: normal;
    font-style: normal;
    font-display: swap;
}

@font-face {
    font-family: 'Lora';
    src: 
        url('/font/Lora-Italic-webfont.woff2') format('woff2'),
    url('/font/Lora-Italic-webfont.woff') format('woff'),
    url('/font/Lora-Italic-webfont.ttf') format('truetype');
    font-weight: normal;
    font-style: italic;
        font-display: swap;
}

@font-face {
    font-family: 'Lora';
    src:
        url('/font/Lora-Bold-webfont.woff2') format('woff2'),
    url('/font/Lora-Bold-webfont.woff') format('woff'),
    url('/font/Lora-Bold-webfont.ttf') format('truetype');
    font-weight: bold;
    font-style: normal;
        font-display: swap;
}

前面我们说过,@font-face 引入的字体文件并不代表页面加载的时候就会下载相应的字体文件,只有满足一定条件才会被下载,比如说被引用了。不过,你可能会碰到只有一两个字或词会用到粗体或斜体的情景,为此引用了独立的粗体或斜体的字体文件。为了个别文字渲染成粗体或斜体,浏览器也会将整个字体文件下载。

事实上,浏览器可以自己制作 粗体斜体 的字体,这被称为 伪粗体(Faux Bold) 和 伪斜体 (Faux Italic)。这也意味着你只需要加载一个常规字体文件即可(前提是设计师能接受 FOFT 的文本渲染现象)。

浏览器自己制作的粗体和斜体与设计师提供的字体可能存在一定的差异,比如:

对于某些字体,这种差异是很大的,尤其是那种很花哨的字体。我们可以通过删除 @font-face 声明来检查你的字体,除了常规的字体版本之外,其他字体都可以删除,然后截取渲染之后的效果图来做对比,就能轻易发现它们之间的差异。

另外,大多数标准西文字体都会包含粗体和斜体的变体,但许多新颖的字体(或 Web Fonts,对于 Web Fonts 取决于设计该字体的设计师)是不包括这些(粗体和斜体的变体)。比如,用于中文、日文、韩文或其他语标文字的字体往往不含这些变体,同时,从默认字体中生成,合成这些变体(即伪粗体或伪斜体)可能会妨碍文本的易读性。在这些情况之下,最好是关闭浏览器默认的 font-synthesis: none 字体合成特性。CSS 的 font-synthesis 特性可以控制浏览器全成哪些缺失的字体,粗体或斜体,该属性值可以是 noneweightstylesmall-capsweight style small-caps 等 。

虽然说使用伪粗体和伪斜体可以减少字体文件的加载,但它也会造成 FOFT 以及妨碍文本易读性。不过也有一种情况下,使用伪粗体或伪斜体可能真的有用,而且是合理的。比如,像 @Zach Leatherman 为 CSS-Tricks 网站做的字体加载策略。这意味着,你正在将你的 Web Fonts 分解成一个较小的部分和一个较大的部分,且使用懒加载。先加载普通版本的字体,并在真正的粗体字体加载之前向用户显示伪粗体。通过这种方式,你可以在你的 Web Fonts 字体被加载后,文字重新流动时,稍微缓解一下文字在页面上的偏移。

如果你对伪粗体或伪斜体相关话题感兴趣的话,还可以阅读 @Marcin Wichary 的 《Crafting link underlines on Medium》或 @Laura Franz 的 《Avoiding Faux Weight And Styles With Google Web Fonts》。

使用可变字体

可变字体是数字时代的一种字体技术。 @John Hudson 对可变字体是这样解说的

可变字体文件是一种字体文件,它的行为类似于多种字体!

上一节我们聊到,在 Web 上使用字体时,可能会因为字体的粗细、斜体等可变体加载多个不同的字体文件,特别是使用 Web Fonts情况下,加重了网页加载的负荷,甚至直接会影响页面加载性能,影响用户的体验。现在,我们可以在一个 OpenType 可变字体文件中,存储多种字体样式,也就是说, 可变字体将字体所有这些变化都存储在一个字体文件中,并且字体的大小相对较小。与静态字体相比,可变字体允许你在一个范围内使用字体的字宽、字重和样式等: ​

比如上图中的 Roboto Flex 字体,如果不使用可变字体,满足上面字体设计效果,可能需要多个不同的静态字体文件,而使用可变字体,我们只需要一个字体文件即可:

可变字体除了只使用一个字体文件就可以满足不同字体样式风格之外,它的文件体积还很小。这是因为可变字体可以包含多个轴,比如 字重(Weight)字宽(Width)斜体(Italic)倾斜(Slant)光学尺寸(Optical Size)自定义轴 。在这些轴上调整值就可以达到不同的字体效果,比如下面这个视频,就是调整字重和字宽时效果:

时至今日,所有现代浏览器都支持可变字体,比如 Firefox 浏览器开发者工具,还提供了直接调整可变字体的控制面板:

假设你已经知道了如何获取可变字体,比如你的设计师为你提供了可变字体或者直接在线上(比如,Variable FontsGoogle Fonts等),你也可以从这个列表清单中获取你可变字体。如果你想把可变字体运用于 Web 中,那也需使用 @font-face 声明来引入,但有两个新的改进:

@font-face {
    font-family: 'Roboto Flex';
    src: url('RobotoFlex-VF.woff2') format('woff2 supports variations'),
    url('RobotoFlex-VF.woff2') format('woff2-variations');
    font-weight: 100 1000;
    font-stretch: 25% 151%;
}
  • 源格式:如果浏览器不支持可变字体,我们不希望它下载字体,所以需要添加一个格式来告诉浏览器。这里有两种语法方式,一种是未来的方式,即 format('woff2 supports variations');另一种是即将废弃的格式format('woff2-variations')。如果浏览器支持可变字体并且支持将来的语法,它将使用第一个声明,如果它支持可变字体和当前的语法,它将使用第二个声明,但它们都指向同一个字体文件
  • 样式范围:你会注意到,在 @font-face 声明块中为 font-weightfont-stretch 提供了两个值。我们现在不是告诉浏览器这个字体提供的具体字重(比如 font-weight: 700),而是告诉它这个字体所支持的字重范围(比如,上面示例是 100 ~ 1000 ,CSS 的 font-weight 值可以是 100 ~ 1000 范围的任意值,如, font-weight: 450)。字宽的范围同样映射在 font-stretch 属性上,它的使用和 font-weight 相似

一个可变字体,常见的注册轴有五个,分别是字重、字宽、斜体、倾斜 和 光学尺寸。每个注册轴都有一个对应的四个字母标记,也可以相应的映射到现有的 CSS 属性上:

注意:注册轴不是必须的特性。包含哪些注册轴主要取决于字体设计器。

除了注册轴之外,字体设计器还可以包含自定义轴。自定义轴让可变字体变得更具创造性,因为不限制自定义轴的范围、定义或数量。与注册轴类似,自定义轴具有相应的四个字母标记。但是,自定义轴的字母标记必须是大写的。例如,你定义了一个注册轴是grade,其对应的字母标记是 GRAD

如果可变字体同时具备多个注册轴,那么在实际使用的时候,可以使用 CSS 的 font-variation-setting 来同时在文本上运用,比如:

:root { 
    --text-vf-wght: 400; 
    --text-vf-wdth: 95; 
    --text-vf-slnt: 9; 
    --text-vf-ital: 0; 
    --text-vf-opsz: 80; 
} 

p { 
    font-family: "Amstelvar VF", Helvetica, sans-serif;
    font-variation-settings:
        "wght" var(--text-vf-wght), 
        "wdth" var(--text-vf-wdth), 
        "slnt" calc( var(--text-vf-slnt) * -1), 
        "ital" var(--text-vf-ital), 
        "opsz" var(--text-vf-opsz); 
}

/* 上面 font-variation-settings 也可以拆解成下面这五个 CSS 属性 */
p { 
    font-weight: var(--text-vf-wght); // 对应“wght”注册轴 
    font-stretch: calc(var(--text-vf-wdth) * 1%); // 对应的是"wdth"注册轴 
    font-style: oblique calc(var(--text-vf-slnt) * 1deg); //对应的是"slnt"注册轴 
    font-style: italic; // 对应的是"ital"注册轴 
    font-optical-sizing: auto; //对应的是"opsz"注册轴 
}

@font-face {
    font-family: "Amstelvar VF";
    src: 
        url("/Amstelvar-Roman-VF_copy.woff2") format("woff2 supports variations"), 
        url("/Amstelvar-Roman-VF_copy.woff2") format("woff2-variations");
    font-display: swap;
    font-weight: 100 900;
    font-stretch: 75% 125%;
}

效果如下

注意,font-variation-settingsCSS Fonts Module Level 4 规范中的一个新特性,如果你对这个属性感兴趣的话,还可以阅读:

可变字体的使用减少字体文件加载的有效手段之一,但其缺点是,使用的 Web Fonts 是可变字体,也就是说在设计师设计该字体的时候,就需要考虑字体相关的注册轴,会增加相应的设计成本。另外还存在一个小小的风险,那就是老的浏览器可能不支持可变字体,不过我们可以使用 @supports 对可变字体做降级处理:

/* Set up Roboto for old browsers, only regular + bold */
@font-face {
    font-family: Roboto;
    src: url('Roboto-Regular.woff2');
    font-weight: normal;
    font-display: swap;
}

@font-face {
    font-family: Roboto;
    src: url('Roboto-Bold.woff2');
    font-weight: bold;
    font-display: swap;
}

body {
    font-family: Roboto;
}

.super-bold {
    font-weight: bold;
}

/* Set up Roboto for modern browsers, all weights */
@supports (font-variation-settings: normal) {
    @font-face {
        font-family: 'Roboto';
        src: url('RobotoFlex-VF.woff2') format('woff2 supports variations'),
            url('RobotoFlex-VF.woff2') format('woff2-variations');
        font-weight: 100 1000;
        font-stretch: 25% 151%;
        font-display: swap;
    }

    .super-bold {
        font-weight: 1000;
    }
}

放弃字体图标

图标在 Web 中的运用是必不可少的,而且图标运用到 Web 中的技术手段也有很多种,比如 img 图标、字体图标、Base64 DataURI 和 SVG 等。早在 2015 年,我在 《Web 中的图标》 一文中对这几种图标使用方式的利弊做相简单的剖析。即使到今天为止,还有很多开发都在使用字体图标(比如集团的 IconFont、社区的 Font Awesome),甚至是还在使用原始的方式,即每个图标对应一个图片文件,使用 <img>background-image 引入:

当然,并不能说使用这些技术就不好,只能说,在当下已不是最佳的技术。比如说,使用 <img> 我们要考虑图片的各种优化;使用字体图标的话,意味着你的图标在下载了一个(通常)大的字体文件(虽然IconFont和Font Awesome可以按需加载图标,减少了字体文件体积)后才会渲染,而且有时字体文件未能及时下载时,会出现一个难看符号( ⃞,一个方框)而不是你想要的图标。而且,字体图标对于性能和可访问性来说都不是较好的做法。如果你在 Web 中真的会有使用图标的场景,到目前为止,最佳的方式是使用 SVG,即直接将 SVG 图标的代码内联到 HTML中或在 React,Vue 开发框架上采用工程能力,比如 Webpack,Vite等,自动将.svg 文件对应的代码内联到 HTML中。

使用 SVG 图标,除了能避免字体文件加载之外,还可以避免图标渲染成其他的形状,并且 SVG 图标是矢量图标,它的适配性非常的强,也可以使用 CSS 来直接控制图片大小,甚至颜色等。还有一点就是,它可以直接将 SVG 图标以代码的形式内联到 HTML中,避免额外的请求。

尽可能的使用系统字体

Web Fonts 受欢迎是因为它们允许设计师在不同的浏览器上保持一致的外观和感觉,给自己的设计提供更多更好的创意。虽然在美学上具有优势,但在性能加载以及用户体验方面,Web Fonts 并没有任何的优势,比如 Web Fonts 引起的 FOUT 和 FOIT 是会造成布局偏移的,从而引起页面的重排和重绘,这对页面的渲染性能来说是致命的。

在介绍 “系统字体 vs. Web 字体”一节中,就多次提到过,如果无必要,请不要使用 Web Fonts,因为系统字体是渲染文本最快方法。即使用 Web Fonts,应该也尽可能的提供一个和 Web Fonts 相似的系统字体作为其备用值。

使用系统字体意味着文本能尽可能快的呈现给用户。我们也有一些方法使字体和操作系统相匹配:

html {
    font-family: "Segoe UI", -apply-system, BlinkMacSystemFont, Roboto,"Helvetica Neue",Arial,"Noto Sans","PingFang SC", "Hiragino Sans GB", "Heiti SC", STXihei, "Microsoft YaHei", SimHei, "WenQuanYi Micro Hei",sans-serif,
}

优化字体文件

如果在开发 Web 的时候,Web Fonts 是必不可少的话,那么我们就要想办法让字体文件变得尽可能的小,以确保其快速下载。也就是说,我们需要对字体文件做一些优化。常见的方式有 字体格式字体子集

使用现代的字体格式

字体常见的格式主要有 .ttf.otf.eot.woff.woff2.svg 等。同一字体,使用不同的格式,其文件大小还有是明显差异的,.woff2 格式相对要小:

不同格式的字体文件的大小有差异,主要是它们各种采用的压缩算法不同:

  • .ttf.otf"原始" 字体文件,未经地压缩,如果不需要兼容古老的浏览器的话,它们就不应该被 @font-face 引用
  • .eot 支持子集,并使用 LZ 压缩
  • .woff 使用的是 Gzip 压缩
  • .woff2 使用的是 Brotli 压缩

在实际使用的时候,我们只需要使用 .woff2 格式即可,因为时至今日,该格式已经得到了众多主流浏览器支持

而且,.woff2 格式比 .woff 格式字体小 20% ~ 30% ,甚至更多。另外,使用 .woff2 格式,还有利于我们其他的一些优化策略,比如字体子集。

@font-face {
    font-family: Roboto;
    src: url('Roboto-Regular.woff2');
    font-weight: normal;
    font-display: swap;
}

如果条件允许,应该在 .woff2 的字体格式上尽可能提供可变字体,这样除了使用现代字体格式之外,还可以减少字体文件加载:

@font-face {
    font-family: Roboto;
    src: url('Roboto-Regular.woff2');
    font-weight: normal;
    font-display: swap;
}

@font-face {
    font-family: Roboto;
    src: url('Roboto-Bold.woff2');
    font-weight: bold;
    font-display: swap;
}

@supports (font-variation-settings: normal) {
    @font-face {
        font-family: 'Roboto';
        src: url('RobotoFlex-VF.woff2') format('woff2 supports variations'),
            url('RobotoFlex-VF.woff2') format('woff2-variations');
        font-weight: 100 1000;
        font-stretch: 25% 151%;
        font-display: swap;
    }
}

字体子集

许多字体会有来自多个字母的字形(字形是单个字符,比如 a&),如果你的网站是一个纯英文的网站,可能只会用到拉丁字母(a~Z0~9 或一些基本字符,比如 +- 等),并且不使用连字符(比如 é),那么这些字形在你的字体文件中就是多余的,如下图所示:

这样就可以生成一个新的较小的字体文件,其中只包括我们需要的字形。

如果你需要对一个字体文件进行子集的优化,可以使用一些工具来完成,比如 Everything Fonts上的Font Subsetter工具@Munter 的 subfont。另外,还可以使用Glyphhanger工具,用来检查你网站上的每一个页面是否有不同的字形。Glyphhanger是一个命令行工具,它做两件事:

  • 它查看你的网页并确定所使用的Unicode字符范围(这些范围对应于脚本或语言,例如 "基本拉丁文"、"西里尔文"、"泰文");
  • 它对字体文件进行子集,输出一个只包含指定范围内字符的新版本。

要开始使用Glyphhanger可能有点困难(你需要懂点Python和pip)。@Sara Soueidan 在他的教程《How I set up Glyphhanger on macOS for optimizing and converting font files for the Web》中阐述了如何在macOS上设置Glyphhanger,以便为 Web 优化和转换字体文件。

字体子集是优化字体文件大小的有效手段之一,但它也有一些潜在的缺点。比如,你正在建立一个显示用户生成的内容、人名或地名的网站,你应该考虑26个标准字母(a~Z)、10个数字(0~9)和英语写作中常见的少数符号以外的字符(比如+-$ 等)。至少,你应该考虑到变音符:出现在字符上方或下方,改变其发音的字形。这在包括法语、西班牙语、越南语以及希腊语或希伯来语等字母的音译(或 "罗马化")文本中很常见;它们也出现在借词(从另一种语言采用的词语)中。如果你过于积极地进行子集,你甚至可能在同一个词中出现各种字体的混合。

还有,要是你的网站支持多国语言,就有可能为你的字体创建多个子集变化。在这种情况下,在你的@font-face规则中使用unicode-range声明,让浏览器知道哪些字符在哪个字体文件中。比如:

/* cyrillic */
@font-face {
    font-family: 'Open Sans';
    font-style: normal;
    font-weight: 400;
    font-stretch: 100%;
    font-display: swap;
    src: url(/font/open-sans.woff2) format('woff2');
    unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

/* greek */
@font-face {
    font-family: 'Open Sans';
    font-style: normal;
    font-weight: 400;
    font-stretch: 100%;
    font-display: swap;
    src: url(/font/open-sans.woff2) format('woff2');
    unicode-range: U+0370-03FF;
}
/* hebrew */
@font-face {
    font-family: 'Open Sans';
    font-style: normal;
    font-weight: 400;
    font-stretch: 100%;
    font-display: swap;
    src: url(/font/open-sans.woff2) format('woff2');
    unicode-range: U+0590-05FF, U+20AA, U+25CC, U+FB1D-FB4F;
}

有关于 @font-face 声明块中使用 unicode-range更多的介绍,还 可以阅读:

如果你开发的应该是一个中文应用,而且会用到非系统字体,那么字体的子集就非常有用了。因为中文字体包要比其他字体包大得多,而且我们可能只是会用到部分中文汉字,比如说标题。

正如上图所示,“前端练习生计划”标题使用的是“方正兰亭大黑简体”,“计划介绍”使用的是“方正汉真广标简体”。针对这种知道使用的文字(文字内容是固定的),那就可以使用字体的子集,可以让中文字体文件变得小很多。我们可以使用一些工具,快速构建出所需内容的字体子集,比如 font-extractorfont-spider-plus(字蛛)。我们以字蛛为例。

在本地构建一个项目,将中文字体文件放置到该项目中,并且创建一个 index.html 文件,通过 @font-face 把字体引入到相应的 <style> 中,并且运用到指定文字内容的元素上,同时在 HTML 中将需要的文字内容输入到某个元素中:

类似上图操作完成之后保存 index.html 文件,然后在命令终端上进入该项目下,执行下面的命令:

❯ font-spider *.html​

此时字蛛会根据指定的内容和字体创建字体子集:

生成的字体子集对应的还是 .ttf 文件,我们前面说过,如果不需要兼容低版本浏览器的话,我们应该尽可能的使用 .woff2 字体。因此,我们还需要使用字体转换工具,将 .ttf 转换成 .woff2 字体:

将转换出来的 .woff2 应用到 @font-face 声明块中:

@font-face {
    font-family: "FZHZGBJW";
    src: url("FZHZGBJW.woff2") format("woff2");
    font-weight: normal;
    font-style: normal;
    font-display: swap;
}

@font-face {
    font-family: "FZLTDHJW";
    src: url("FZLTDHJW.woff2") format("woff2");
    font-weight: normal;
    font-style: normal;
    font-display: swap;
}   

这样一来,FZLTDHJWFZHZGBJW 就可以运用到指定的元素上了。但需要注意的是,如果被用到其他文本上,将不会有效果:

这种方案只适合在指定的文字内容中使用,如果你的文本内容是动态生成的或由用户动态输入的,那么字体子集方案就不适用了!就拿聚划算首页来说,像下图这种固定的文字内容,我们就可以使用字体子集技术方案:

注意:与改变文件格式一样,要确保你的字体的许可证允许子集。

如果你对字体字集技术感兴趣的话,还可以阅读《Creating font subsets》一文。

字体加载

浏览器加载字体的速度对于 Web 的性能以及 Web 中使用 Web Fonts 的渲染都很重要,Web Fonts 加载速度(所耗时)对于 font-display 的决策也有着直接影响。如果你的 Web 中使用了 Web Fonts的话,必须确保浏览器能够尽快下载字体文件。也就是说,我们要采用一些策略,让浏览器下载字体文件变得更快。

自己托管字体

字体的托管分为 自我托管第三方托管 两种:

<!-- 自我托管 -->
<head>
    <style>
        @font-face {
            font-family: 'Google Sans';
            src: url("GoogleSans-Regular.woff2") format('woff2');
            font-display: swap;
        }
        body {
            font-family: system-ui;
            font-size: 1em;
        }
        h1 {
            font-family: 'Google Sans', sans-serif;
            font-size: 3em;
        }
    </style>
</head>

<!-- 第三方托管 -->
<head>
    <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=Zen+Tokyo+Zoo&display=swap" rel="stylesheet">
    <style>
        body {
            font-family: system-ui;
            font-size: 1em;
        }
        h1 {
            font-family: 'Zen Tokyo Zoo', sans-serif;
            font-size: 3em;
        }
    </style>
</head>

不过,使用第三方服务会带来性能上的损失。比如,谷歌字体的嵌入过程使用了一个CSS <link>@import,CSS文件会返回一组动态的 @font-face规则。这些规则对于给定的字体、用户代理和任何额外的查询参数(如子集和字体显示选项)都是不同的。

使用第三方服务意味着你的字体会被延迟。最好的情况是你直接从另一个主机名(例如fonts.gstatic.com)请求字体文件,这将产生连接成本,比比如,DNS查询、TCP连接和TLS协商。最糟糕的情况是多跳,比如从fonts.googleapis.com 加载一个CSS文件,而这个文件引用了fonts.gstatic.com上的文件,这就会产生两个连接费用。不过,在某些情况下,谷歌字体的成本可能会被其带来的好处所抵消,因为他们提供的字体经过了很好的优化,并自动按照字母表进行了子集。

如果你确认使用的 Web Fonts 托管在第三方服务上,建议使用下面两种优化策略,尽可能快地加载第三方字体:使用内联字体声明使用预连接资源提示(preconnect

  • 内联字体声明:在主文档中内联声明字体(font-family),而不是在外部样式表中声明字体,这样做可以让浏览器确定哪些字体文件将在页面上使用,而不必等待一个单独的样式表文件下载。这一点很重要,因为一般来说,浏览器在知道页面上使用了字体文件之前不会下载这些文件。在大多数情况下,内联字体声明比使用预加载(preload)来加载字体更可取。
  • 预连接资源提示:Google 字体服务推荐的加载谷歌字体的方法是结合使用<link>标签和预连接资源提示(preconnect)。预连接资源提示建立了与第三方来源的早期连接。在上面的代码片段中,第一个资源提示为下载字体样式表建立了一个连接(<link rel="preconnect" href="[https://fonts.googleapis.com">](https://fonts.googleapis.com">));第二个资源提示为下载字体文件建立了一个连接(<link rel="preconnect" href="[https://fonts.gstatic.com"](https://fonts.gstatic.com") crossorigin>)。

一般来说,你应该从你的域名提供字体,以避免连接到第三方域名的成本,这在高延迟的连接中尤其重要。正如上面示例代码所示,即使你决定使用自己的域名来托管字体文件,你也应该尽可能的执行下面两条策略,可以让浏览器下载字体文件更快:

  • 内联字体声明:这一点和使用第三方托管字体文件一样,尽可能在主文档内联样式表中使用 @font-facefont-family 声明字体
  • 使用现代字体格式 woff2,尽可能使用字体子集,并且在 @font-face 使用 unicode-range 指定字符集

另外,自我托管字全还具有以下几个优势:

  • 性能:域名查询需要时间;你可以使用预连接资源提示(preconnect)来缓解这个问题,但打开一个新域名的TCP连接,总会有性能上的损失。这可能是为什么谷歌自己的一些网站(包括web.dev)现在使用自我托管的字体(Request URL: [https://web.dev/fonts/google-sans/bold/latin.woff2](https://web.dev/fonts/google-sans/bold/latin.woff2))而不是谷歌字体。
  • 隐私:像Adobe Fonts这样的付费 Web Fonts 服务需要检测页面浏览量以进行结算,它们可能会收集用户的一些数据。如果你有选择,请使用CSS(<link rel="styleheet">)而不是JavaScript(<script>)来加载你的字体,以尽量减少第三方能够收集到的用户数据。
  • 控制:使用自我托管的字体,你就可以完全控制你的字体的加载方式,允许你提供自定义的子集,定义font-display设置,并指定浏览器应该缓存多久的字体文件。
  • 可靠性:第三方服务可能会出现速度减慢、中断或完全关闭的情况。当自我托管你的字体时,只要你的网站在运行,你的字体就可以使用。

缓存字体

字体可以被缓存在 客户端CDN 这两个地方。客户端上的缓存对于会话中的导航非常重要,而且应该以避免重新验证请求的方式来完成。重新验证请求(if-not-modifiedif-modified-since)会阻止浏览器使用字体文件,直到它确认该文件在服务器上没有改变。字体很少为化,所以我们应该实现如下的缓存头,如果字体变化破坏了缓存,就更新文件名。

cache-control: public,max-age=31536000,immutable

这就告诉浏览器,他们可以将字体保留一年之久,而且不需要重新验证(Firefox和Safari支持immutable,Chrome应该会自动避免重新验证请求)。避免在这些响应中添加 ETags,因为它们可能会强制进行重新验证。

字体预加载

在字体文件优化一节中我们知道,@font-face 规则允许我们使用 unicode-range 和 字体变量等技术将一个字体家族分割为字体子集。有了这些声明,浏览器就会找出所需的子集或字体变体,并下载渲染文本所需的最小字体包,这非常方便。然而,如果你不小心,它也可能成为关键渲染路径阻塞性资源,给 Web 性能带来一定的瓶颈,并且会延迟文本的渲染。

有关于“关键渲染路径”和如何“消除阻塞页面渲染资源”更多的介绍,可以阅读《关键渲染路径(CRP)》和《消除阻塞页面渲染的资源》!

延迟加载字体有一个重要的潜在影响,它可能会延迟文本的渲染:浏览器必须依赖于 DOM 和 CSSOM 树来构建渲染树,然后才知道渲染文本需要哪些字体资源。因此,字体请求会延迟到其他关键资源之后,浏览器可能会被阻止渲染文本,直到字体资源被获取。

**延迟加载 Web Fonts 是一种很不好的方案! **

这也是浏览器加载资源的一种默认行:

  • ①:浏览器请求HTML文档
  • ②:浏览器开始解析HTML并构造DOM树
  • ③:浏览器发现CSS、JS和其他资源并分派请求
  • ④:浏览器在接收到所有CSS内容后构造CSSOM树,并将其与DOM树组合起来构造渲染树。在渲染树指示需要哪些字体来渲染页面上的指定文本之后,就会发送字体请求
  • ⑤:浏览器执行布局并将内容绘制到屏幕上。如果字体还不可用(正在加载或加载失败),浏览器可能无法渲染任何文本像素;字体可用后,浏览器绘制文本像素

有关于这方面的详细解读请参阅《初探 CSS 渲染引擎》一文!

这样一来,浏览器会造成使用 Web Fonts 的文本不渲染。即,页面内容的第一次绘制(可以在渲染树构建后不久完成)和对字体资源的请求之间的“竞争”造成了“空白文本问题”,浏览器可能渲染页面布局,但忽略了任何文本(这里的文本指的是使用了 Web Fonts 来渲染的文本)。

注意,内联 CSS 不需要网络请求,这意味着字体可以在页面加载的早期就获得所需字体资源!

如果我们知道在页面上渲染文本肯定需要一种 Web Fonts ,我们就可以通过 预加载 Web Fonts 和 **使用 ****font-display** 来告诉浏览器,我们需要尽快下载这种字体。或者说,我们可以使用它们来控制浏览器在使用不可用字体时行为,可以防止由于字体加载而产生的空白页面(FOIT)和布局偏移(CLS)。即 使用预加载资源提示:

<link rel="preload" href="/typefesse.woff2" as="font" type="font/woff2" crossorigin>

使用 <link rel="preloadd> 会让浏览器在关键渲染路径的早期触发对 Web Fonts 的请求,而不必等待 CSSOM 树被创建。即 通过添加这个标签,可以告诉浏览器立即开始加载所需的字体资源,而通常情况,浏览器要等到在你的 CSS 中找到对 Web Fonts (特定字体)的引用并找到使用该字体的 DOM 元素时才会开始下载。浏览器通常很聪明,只在当前页面需要字体的时候才会下载它们。使用预加载(peload)会覆盖这种行为,迫使浏览器下载字体,即使它没有被使用。

虽然说预加载提示(preload)可以告诉浏览器立即下载你指定的 Web Fonts资源,但并不代表你可以无限制的使用 preload ,因为你预加载字体越多,你从这种技术中得到的好处就越少。因为预加载的请求是高优先级的。比如下图,有五个字体文件被加载,其中一个阻塞了页面 CSS(可能是因为 <link rel="preload"> 被放置在 <link rel="stylesheet"> 之前),共他的则阻塞了页面的主要 JavaScript 包(JavaScript Bundle):

因此,在使用预加载字体时,应该确保你的预加载标签(<link rel="preload" as="font" >)放置在页面关键渲染资源的后面,并限制在两到三个字体文件,以获得最佳效益。甚至是只对首屏幕用到的 Web Fonts 资源使用 preload 做预加载。

这里推荐两篇有关于 peloadfont-display 对 Web Fonts 加载优化的文章:@Harry Roberts 的 《The Fastest Google Fonts》和 @Andydavies 的 《Preloadig Fonts and the Puzzle of Priorities》。

如果我们在 @font-face 规则中引入的 Web Fonts 和自己的网站不在同一个域名下,比如说像下面这样,放置在另一个 CDN上:

@font-face {
    font-family: AlibabaSans102;
    font-weight: 700;
    font-style: normal;
    font-display: swap;
    src:url('https://g.alicdn.com/eva-assets/8f07c38aa173457f747f15a8774161a4/0.0.1/tmp/font/0ce464d2-bb11-41c8-8470-0049cea5f6b1.woff2') format('woff2');
}

我们应该像使用第三方字体托管那样,在 <link> 标签中使用 preconnect 链接到 CDN 的域名下([g.alicdn.com](https://g.alicdn.com)),它会通过附在 [g.alicdn.com](https://g.alicdn.com) 响应上的一个 HTTP 标头抢先 连接到[g.alicdn.com](https://g.alicdn.com)

<head>
    <link link rel="preconnect" href="https://g.alicdn.com" crossorigin />
    <!-- Preload 的 Web Fonts 资源应该放置在页面关键渲染资源之后 -->
    <link rel="preload" importance="high" href="https://g.alicdn.com/eva-assets/8f07c38aa173457f747f15a8774161a4/0.0.1/tmp/font/0ce464d2-bb11-41c8-8470-0049cea5f6b1.woff2" as="font" type="font/woff2" crossorigin="anonymous">
</head>  

preconnect 允许浏览器在向服务器实际发送 HTTP 请求之前提前建立连接。这包括 DNS 查询、TLS 协商 和 TCP 握手。这反过来又消除了往返的延迟,为用户节省了时间。有关于这方面更详细的介绍,可以阅读《Preload, prefetch and other tags》一文。

如此一来,我们可以结合 peloadpreconnectfont-display 分两种模式来加载 Web Fonts。如果能确定 Web Fonts 会出现在首屏幕的渲染的文本中可以按下面方式来加载 Web Fonts 和使用:

<head>
    <!-- 如果 Web Fonts 文件存放的地址和主站域名相同,可以忽略 preconnect的使用,否则建议使用 preconnect 提前连接到 Web Fonts 存放的 CDN 域名 -->
    <link rel="preconnect" href="https://g.alicdn.com" crossorigin />
    
    <!-- 使用 preload 预加载 Web Fonts,但需要放置在页面渲染的关键资源之后,比如首屏关键 CSS 之后 -->
    <link 
            rel="preload" 
            importance="high" 
            href="https://g.alicdn.com/font/AlibabaSans102.woff2" 
            as="font" 
            type="font/woff2" 
            crossorigin="anonymous" />
    
    <!-- 将 @font-face 声明块放在内联 style 中 -->
    <style>
        @font-face {
            font-family: AlibabaSans102;
            font-weight: 700;
            font-style: normal;
            font-display: swap;
            src: url('https://g.alicdn.com/font/AlibabaSans102.woff2') format('woff2'); /* 如果字体文件在主域名下,可以 url('./font/AlibabaSans102.woff2') format('woff2') */
        }

        .price {
            font-family: AlibabaSans102, sans-serif; /* 备用字体最好能和 AlibabaSanns102 接近*/
        }
    </style>  
</head>  

如果 Web Fonts 存放的位置和主站域名不是同一个,比如说 Web Fonts 放置在 CDN 上(比如,g.alicdn.com),而且 Web Fonts 有可能不会出现在页面首屏的文本上,那么我们可以借鉴 Google Fonts 优化方案。首先把 @font-face 规则都放置在同一个 CSS 文件中,比如 font.css

@font-face {
    font-family: AlibabaSans102;
    font-weight: 700;
    font-style: normal;
    font-display: swap;
    src: url("https://g.alicdn.com/fonts/AlibabaSans102.woff2") format("woff2");
}

并且尽可能地把 font.css 和 Web Fonts 放置在同一个域名或 CDN 地址(比如 g.alicdn.com)。这样一来就可以在 HTML 文档的 <head> 标签中按下面的代码来使用 Web Fonts:

<head>
    <!-- ① :使用 preconnect,提前连接到 g.alicdn,浏览器向服务器发送 HTTP 请求之前就建立连接 -->
    <link rel="preconnect" href="https://g.alicdn.com" crossorigin">

    <!-- ② :使用 preload,告诉浏览器立即加载 font.css 文件,为 CSS 文件启动一个高优先级的异步加载。适用于大多数现代浏览器。同样的,该标签应该尽可能的放置在页面渲染关键资源的后面  -->
    <link rel="preload" as="style"  href="https://g.alicdn.com/font.css?family=AlibabaSans102&display=swap" />

    <!-- ③: 使用媒体查询启动一个低优先级的异步加载,文件加载成功之后才会应用到页面上,在所有启用了 JavaScript 的浏览器中工作(它是异步加载 CSS 的一种技术方案) -->
    <link rel="stylesheet" href="https://g.alicdn.com/font.css?family=AlibabaSans102&display=swap" media="print" onload="this.media='all'" />

    <!-- ④: 如果访问者故意禁用了页面的 JavaScript(不太可能出现),那么就会回退到原来的方法。好消息的是,尽管这是一个阻塞渲染的请求,但它仍然可以被利用;预连接使得它比默认的方法略微快一些 -->
    <noscript>
        <link rel="stylesheet" href="https://g.alicdn.com/font.css?family=AlibabaSans102&display=swap" />
    </noscript>
</head>

特别声明:该方案号称是用于高性能 Google Fonts 的最佳方案。我们把该方案搬到我们的业务中,帮助我们在使用 Web Fonts 有一个较好的性能优化方案!

在上面的代码中,在运用 preload<link> 标签上使用了 importance="high" ,用来告诉浏览器,将资源加载的优先级提到高(high)。这是 Priority Hints 提案中的特性,该提案于 Chrome 96 开始进入初期试用中

  • 它包括资源标签(比如 <link><img><script><iframe> 等)上的 importance 属性,可以设置为 lowhighauto ,允许用户进一步细化控制资源优先加载的等级,可以在加载时不阻塞渲染以及更关键的资源。这也算是 消除阻塞页面渲染的资源 的一种优化策略吧!
  • 在浏览器中,资源获取存在优先级(Resource Priority)的相关概念:
    • Chrome 的资源优先级有如下规则
      • 优先级最高(Highest):HTML 和 CSS;
      • 优先级为高(High):字体、<head> 中的 <script>XMLHttpRequestfetch() 请求;
      • 优先级为中(Medium):首屏图片、<body> 底部的 <script>
      • 优先级为低(Low): 非首屏图片、<script async><script defer>
    • 可用 rel="preload" 来将资源加载的优先级提到高(High);
    • 可用 rel="prefetch" 来将资源加载的优先级降低到最低(Lowest);
    • 和上述手段的区别是,importance="high"importance="low" 只是会把资源优先级进行相对原优先级的调整,粒度相对更细,它是对原有的 rel="preload" 等手段的扩展,可搭配或单独使用;
  • 同时,Priority Hints 也有适用于 fetch API 的提案,它允许在第二参声明请求的优先级,让浏览器协调 fetch API 网络请求的优先级。

此外,Chrome 的 <script> 标签还存在执行优先级和加载优先级的区分!

CSS Font Loading API 和 FontFaceObserver

<link rel="preload"> 和 CSS 的font-display 一起使用,可以让你很好的控制 Web Fonts 的加载和渲染,而且不需要增加增加很多开销。但是,如果你需要额外的定制,并且愿意承担运行 JavaScript 脚本所带来的额外开销,那你还可以使用 CSS Font Loadig APIFontFaceObserver

字体加载API( CSS Font Loadig API )提供了一些 JavaScript API (比如,FontFaceFontFaceSetFontFaceSourceload()check()ready() 等) 来定义和操作CSS的 @font-face (Web Fonts),跟踪它们的下载进度,并覆盖其默认的懒加载行为。例如,如果你确定需要一个特定的字体,你可以定义它,并告诉浏览器立即启动字体资源的加载。

var font = new FontFace("Awesome Font", "url(/fonts/awesome.woff2)", {
    style: 'normal', 
    unicodeRange: 'U+000-5FF', 
    weight: '400'
});

// 浏览器不需要等待渲染树构建完成就启动了一个立即取回(immediate fetch)
font.load().then(function() {
    // 在字体下载完成后应用该字体(可能会重新渲染文本并导致页面重排,即FOUT)
    document.fonts.add(font);
    document.body.style.fontFamily = "Awesome Font, serif";

    // 或者,默认情况下,文本内容是隐藏的,等字体加载完成才渲染,即 FOIT
    var content = document.getElementById("content");
    content.style.visibility = "visible";

    // 或者其他加载和渲染策略
});

此外,由于你可以检查字体状态(通过check()方法)并跟踪其下载进度,你还可以定义一个自定义的策略来渲染页面上的文本:

  • 你可以保留所有的文本渲染,直到字体可用为止。
  • 你可以为每个字体实现一个自定义的超时。
  • 你可以使用后备字体来渲染,并在字体可用后注入一个使用所需字体的新样式。

最重要的是,你还可以针对页面上的不同内容混合和匹配上述策略。例如,你可以将某些部分的文本渲染推迟到字体可用时,使用一个回退字体,然后在字体下载完成后再重新渲染。

字体加载API在旧的浏览器中是不可用的,如果你决定使用字体加载 API 来决定字体加载策略,并且还需要兼容一些老版本浏览器的话,你可以考虑使用FontLoader polyfillWebFontloader库来提供类似的功能,这样做的主要缺陷是会因为额外的JavaScript依赖而产生更多的开销。

如果真的需要兼容老版本浏览器(比如 IE11 及以下)而放弃使用 字体加载 API(CSS Font Loading API),你也还有另外一个选择,那就是使用 @Bramstein 的 FontFaceObserver

FontFaceObserver 是一个轻便、快速而简单的 Web Fonts 加载器。你可以用来它加载字体,并且控制浏览器加载字体和渲染文本的行为!

FontFaceObserver 就是一个简单地 promise ,你可以使用它来控制 Web Fonts 的加载,而且这个字体可以是自己托管的,也可以是第三方服务商(比如,Google FontsTypekitFonts.comWebtype等)提供的。它的使用很简单,假设在你的 CSS 文件中的某个地方使用 @font-face 规则,引入了一个 Web Fonts:

@font-face {
    font-family: 'Output Sans';
    font-display: swap;
    font-weight: normal;
    font-style: normal;
    src: url(output-sans.woff2) format('woff2');
}

你可以通过创建一个 FontFaceObserver 实例,并且调用它的 .load() 方法来加载这个 Web Fonts:

var webFonts = new FontFaceObserver('Output Sans');

webFonts.load().then(() => {
    documet.documentElement.classList.add('fonts-loaded');
    console.log('Output Sans has loaded.');
})

上面的脚本会加载字体并返回一个 promise ,这个 promise 在字体加载成功之后会 resolved ,如果字体加载失败则会 rejected 。这就是 FontFaceObserver 的全部内容,它比 CSS Font Loading API 要简单地多。不过有一点需要注意,如果你想在不支持 promise 浏览器上使用 FontFaceObserver 的话,就需要加载 fontfaceobserver.standalone.js

在 CSS 的 font-display 还未出现或未得到主流浏览器支持的情况之下,社区中大多数都是采用 JavaScript 脚本(比如,CSS Font Loading API)来处理 Web Font 加载以及控制浏览器渲染文本策略(FOUT 或 FOIT)。比如,2017 年左右,eBay 网站的 Web Fonts 策略就使用 了 CSS Font Loading API

事实上在 Web 开发领域被广泛采用或分享的字体加载策略,还是由业内著名字体方面专家 @Zach Leatherman 早在 2016 写了** Web Font 加载策略大全**,涵盖了:

  • CSS 的 @font-face ,最基本的 Web Fonts 使用方式,
  • CSS 的 font-display ,根据不同的值 (autoblockswapfallbackoptional )控制浏览器渲染文本策略
  • preload (资源提示,字体预加)
  • 不使用 Web Fonts,尽可能使用系统字体达到设计需求
  • 内联 DataURI,将 Web Font 用 DataURI 来表述,并且直接在 @font-facesrcurl() 中引用 DataURI
  • 异步 DataURI,将 Web Font 用 DataURI 来表述,并采用异步加载
  • 带有类名的 FOUT
  • FOFT,或具有两阶段渲染的 FOUT
  • 关键 FOFT(Critical FOFT)
  • 带有 DataURI 的关键 FOFT
  • 带有 preload 的关键 FOFT

这份字体加载策略指南 除了介绍了如何使用(对应的链接)还列出了每一种字体加载策略的利弊。 @Zach Leatherman 还为每种策略都提供了测试案例

@Zach Leatherman 在文章中推荐了下面两种策略:

  • 带有类名的 FOUT:大多数情况下最佳方法,无论字体是自己托管还是第三方托管,都是可行的
  • 关键的FOFT:性能最强的方法,只能用于字体是自己托管

另外,正如 @Zell Liew 在他的博文 《The Best Font Loading Strategies And How To Execute Them》开篇时所说:"@Zach Leatherman 提供的字体加载策略指南(或列表)对于很多 Web 开发者来说会感到困惑或害怕"。如果你并想对每一种字体加载策略进行深究,或者只是想了解 @Zach Leatherman 推荐的这两种策略,那么 @Zell Liew 这篇文章还是很值得你花点时间阅读 ,并且在文章末尾也提供了一些选择字体加载策略建议:

如果 Web Fonts 是第三方托管(云主机提供),就使用 font-display: swap,否则就使用带有类名的 FOUT;如果 Web Fonts 是自己托管,你有几个选择,比如 @font-facefont-display: swap ;带有类的 FOUT;标准的 FOFT;关键 FOFT!

下面是如何选择它们的方法:

  • 如果你不希望因为 JavaScript 带来额外的开销或者你不擅长 JavaScript 的使用,那么 就选择 @font-facefont-display: swap 。这是所有选项中最简单,成本最低的一种策略。特别是对于只使用一两个字体文件更适用
  • 如果你不担心因 JavaScript 带来额外开销,又不想对 Web Fonts 做子集处理,那就选择 标准的 FOFT 策略
  • 如果你想让性能达到极致,就应该考虑使用 关键 FOFT 或 关键 FOFT 变体(Critical FOFT Variant)

就我个人而言,我更赞成 @Lucy 在 2019 年分享的博文 《网页加载字型 Web Font FOIT & FOUT 与效能测试》中提到的观点:

  • 自己托管字体
  • 使用 font-display: swap
  • 最多对三个字体文件使用预加载: preload
  • 如果你的字体文件超过三个,那么对剩下的字体文件使用 preconnect 分区预连接

在不使用 JavaScript 脚本处理字体加载的话,较佳的策略还是推荐:内联CSS(将 @font-face规则内联)+ 3个 preload (最多预加载三个字体文件 )+ 2个分片区 preconnect;还有在@font-face 规则中别遗漏了 font-display!

字体加载检查清单

不管是使用 <link rel="preload">font-display 、CSS Font Loading API 、FontFaceObserver,还是说开发者 使用 JavaScript 编写更符合自己所需的字体加载策略。可以说都是从用户体验(FOUT 或 FOIT)和 Web 性能两个方面找到最佳的平衡点。如果你还在纠结采用何处方式来加载字体以及控制浏览器渲染文本(指的是使用了 Web Fonts的文本),那么可以按照 @Zach Leatherman 的 《The Font Loading Checklist》来进行:

  • 提前下载重要的字体:Web Fonts 在被发现用在内容中时才开始下载,所以在页面加载时往往比较晚。我们需要告诉浏览器更早的开始下载我们的高优先级的 Web Fonts。采用的策略就是:预加载,即 <link rel="preload" />
  • 优先考虑可读文本:使用 FOUT 策略替代 FOIT 策略,在 Web Fonts 加载期间优先考虑系统字体渲染文本。采用的策略是:CSS 的 font-displayCSS Font Loading API
  • 使字体文件变得更小:减少 Web Fonts 加载时间(使用更小的文件下载会更快)。采用的策略是:使用 woff2字体格式、字体子集
  • 减少页面加载过程中的移动:每个独立的 @font-face 规则都有自己的加载生命周期。它自己的 FOIT、自己的 FOUT,自己的重排和重绘。当使用两个或更多 Web Fonts时,重要的是将重绘部分进行分组,以减少页面上的文本重排。采用的策略是:使用 CSS Font Loading API 来分组你的重绘使用可变字体