在大多数情况下,现在的网络字体比以前更快。随着浏览器供应商的FOUT/FOIT行为更加标准化,再加上较新的font-display
规范,性能–也就是用户–似乎终于被放在了前面和中心位置。
人们普遍认为,自我托管的字体是最快的选择:同源意味着减少网络协商,可预测的URL意味着我们可以preload
,自我托管意味着我们可以设置我们自己的cache-control
指令,而完全的所有权减轻了将静态资产留在第三方源上的风险。
尽管如此,像谷歌字体这样的服务的便利性怎么强调都不为过。 他们为特定的用户代理和平台提供尽可能小的字体文件的能力是惊人的,而且有如此巨大的、可免费使用的库,由谷歌级CDN提供……我绝对明白为什么人们继续求助于它。
在这个网站上,性能是游戏的唯一名称,我完全放弃了网络字体,而是选择使用访问者的系统字体。这样做速度快,非常适用于相关的设备,而且几乎没有工程开销。但是,在harry.is上,我真的想把谨慎抛到脑后。让它见鬼去吧!让我们拥有一种网络字体吧
然而,我仍然需要它是快速的。
在进行原型设计时,我求助于谷歌字体,由于他们最近能够通过一个URL参数(&display=swap
)支持font-display
,我知道事情会保持相当快的速度。然后我有了一个想法。
它始于一条推特。
提示:如果你要为你的谷歌字体使用`font-display`,那么异步加载整个请求链是有意义的。pic.twitter.com/k6obyVZGZP
– Harry Roberts (@csswizardry) 11May, 2020
虽然font-display: swap;
是一个巨大的胜利,但仍有很多事情让我感到不安。我还能做什么来使谷歌字体变得_快速_?
不妨说,我最终进入了一个小兔子洞……
测试
我对harry.is和csswizardry.com的主页进行了同样的测试套件。我从harry.is开始,因为那是我使用谷歌字体的地方,但我觉得那是一个太简单的页面,不现实。所以,我把CSS Wizardry的主页克隆了很多遍,并在那里实现了各种不同的方法。在这篇文章的每个部分,我将列出两个网站的结果。我的变体是
- **遗留的。**谷歌字体,没有
font-display
。 font-display: swap;
:使用Google Fonts的新默认值。- **Async CSS。**异步加载Google Fonts文件。
preload
:preload
ing the CSS file to increase its priority.preconnect
: 自己预热fonts.gstatic.com
原点。
此外,每个变体都是加法的–它包括之前的变体以及自己的加法。我没有尝试_只用_ preload
或_只用_async,因为这样做毫无意义–我们知道,组合起来的结果会比任何单独的结果更好。
对于每项测试,我捕捉了以下指标。
- **第一次绘画(FP)。**关键路径受影响的程度如何?
- **第一张内容丰富的图片(FCP):**我们在多长时间内能够真正读懂一些东西,不管它是否是网络字体?
- **第一网络字体(FFF)。**在什么时候加载了第一个网络字体?
- **视觉上完成(VC)。**什么时候一切都稳定下来了(代表,但不等同于最后的网络字体)?
- 灯塔得分。如果它不在Lighthouse上,它还算不算?
注:所有的测试都是使用一个私有的WebPageTest实例进行的(WebPageTest现在已经停机,所以我无法使用公共实例,这意味着我无法分享任何URL–抱歉)。具体的配置文件是一个三星Galaxy S4的3G网络。
为了使这些片段更容易阅读,我将用$CSS
替换所有https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,400;0,700;1,400;1,700
的实例。
默认/传统
谷歌字体在过去一年左右的时间里发挥了非常大的作用。默认情况下,任何新创建的谷歌字体片段都带有&display=swap
参数,该参数将font-display: swap;
注入所有@font-face
at-rules。该参数的值可以是swap
、optional
、fallback
、或block
中的任何一个。你可以在MDN上阅读更多信息。
然而,对于我的基线,我打算将font-display
修剪掉。这是很多网站可能仍在使用的传统格式,它使我们有一个更合适的基线来进行比较。
剪辑。
<link rel="stylesheet"
href="$CSS" />
复制代码
这里有两个关键问题。
- 一个同步的,也就是阻断渲染的CSS文件在第三方源头。
- 一个包含
@font-face
at-rules的文件,没有font-display
descriptors。
这是在同步之上的同步–不是好事。
结果(s) – harry.is:
FP | FCP | FWF | VC | 灯塔 |
---|---|---|---|---|
3.4 | 4.6 | 4.9 | 5.0 | 98 |
结果(s) – CSS Wizardry:
FP | FCP | FWF | VC | 灯塔 |
---|---|---|---|---|
3.4 | 4.3 | 4.4 | 4.4 | 96 |
好了。这是我们的基线。谷歌字体文件是这两个测试中唯一一个阻挡渲染的外部资源,我们可以看到它们都有完全相同的第一次绘制。在这些实验中,有一件事让我感到很高兴,那就是谷歌字体的一致性。
在这一点上,Lighthouse给出了一个错误和一个警告。
- 错误)确保文本在网络字体加载期间保持可见
- 警告)消除阻挡渲染的资源
第一个是没有网络字体加载解决方案的结果(例如font-display
);第二个是同步谷歌字体CSS文件的结果。
现在,让我们开始添加一些渐进式的变化,希望能有所改进。
font-display: swap;
在这个测试中,我又把&display=swap
。实际上,这使得字体文件本身成为异步的–浏览器在swap
ping给网络字体之前,只要它到达,就立即显示我们的回退文本。这意味着我们不会让用户看到任何不可见的文本(FOIT),这使得我们的体验更快、更愉快。
**注意:**只有当你努力定义一个合适的回退字体来显示时才会更令人愉快–在确定使用Open Sans之前闪现满页的Times New Roman,可能会是一个更糟糕的体验。值得庆幸的是,莫妮卡使这个过程不仅简单,而且令人惊讶地有趣。如果没有她,我就无法完成这部分工作。 字体风格匹配器.
片断。
<link rel="stylesheet"
href="$CSS&display=swap" />
复制代码
结果 – harry.is:
FP | FCP | FWF | VC | 灯塔 | |
---|---|---|---|---|---|
3.4 | 3.4 | 4.5 | 5.2 | 99 | |
与基线相比的变化。 | 0 | −1.2 | −0.4 | +0.2 | +1 |
结果 – CSS向导。
FP | FCP | FWF | VC | 灯塔 | |
---|---|---|---|---|---|
3.6 | 3.6 | 4.6 | 4.6 | 95 | |
与基线相比的变化。 | +0.2 | −0.7 | +0.2 | +0.2 | −1 |
我们还没有从关键路径中移除任何阻断渲染的资源,所以我并不期望在第一次绘制时看到任何改进。事实上,虽然harry.is保持不变,但CSS Wizardry却慢了200ms。然而,我们看到的是,在harry.is上,第一次有内容的绘制有了戏剧性的改进–超过了一秒钟第一个网页字体在harry.is上有改进,但在csswizardry.com上没有。视觉上完成的速度要慢200ms。
我很高兴地说,就最重要的指标而言,我们快了700-1200毫秒。
虽然这确实极大地改善了网页字体的渲染时间,但它仍然被定义在一个同步的CSS文件中–我们只能期望从这一举措中获得更多的改善。
可想而知,Lighthouse现在只给出一个警告。
- 警告)消除阻挡渲染的资源
因此,下一步就是要解决同步CSS文件的问题。
引用一下,呃,我自己的话。如果你要为你的谷歌字体使用font-display
,那么异步加载整个请求链是合理的。正是这个最初的想法导致了我的推文–如果我已经有效地使CSS文件的内容异步化,那么让CSS文件本身完全同步化就有点糟糕了。
font-display: swap;
是一个好主意。
非同步CSS
使你的CSS异步化是采用关键CSS的关键技术之一。虽然有许多方法可以实现这一点,但我敢说最简单和最普遍的是Filament Group的打印媒体类型技巧。
这将隐含地告诉浏览器以非阻塞的方式加载CSS文件,只将样式应用于print
。然而,在文件到达的那一刻,我们告诉浏览器将其应用于all
上下文,从而对页面的其他部分进行样式设计。
插图。
<link rel="stylesheet"
href="$CSS&display=swap"
media="print" onload="this.media='all'" />
复制代码
虽然这个技巧非常简单–这也是它如此酷的原因**–但我早就有所保留**。你看,一个常规的、同步的样式表会阻止渲染,所以浏览器会给它分配_最高的_优先级。一个打印样式表(或任何与当前环境不匹配的样式表)会被分配到完全相反的优先级:闲置。
这意味着,当浏览器开始发出请求时,异步CSS文件的优先级往往严重不足(或者说,它的优先级是正确的,但可能远远低于你的预期)。以Vitamix为例,我为他们自己的字体供应商实施了异步CSS。
虽然Chrome可以做异步的DNS/TCP/TLS,但它会在较慢的连接上序列化和停滞非关键请求。
浏览器正在做的正是我们告诉它的事情:以打印样式表的优先级来请求这些CSS文件。因此,在3G连接上,下载每个文件需要9秒钟以上。浏览器几乎把其他所有东西,包括body
内的资源,都放在我们的打印样式表之前。这意味着,在3G网络上,有关页面直到12.8秒后才呈现出它的自定义字体,令人眼花缭乱。
值得庆幸的是,在处理网络字体的时候,这并不是世界末日。
- 无论如何,它们都应该被认为是一种改进,所以我们需要能够在没有它们的情况下应对。
- 我们可以而且应该在没有字体的情况下设计出合适的回退方案,并且。
- 如果我们预期会有如此严重的延迟,我们应该使用
font-display: optional;
。
然而,对于折页下方的CSS来说,近10秒的延迟是不可接受的–几乎可以100%确定用户会在这个时间范围内滚动。
还是那句话!如果我们以异步方式加载谷歌字体,会发生什么?
结果 – harry.is:
FP | FCP | FWF | VC | 灯塔 | |
---|---|---|---|---|---|
1.8 | 1.8 | 4.5 | 5.1 | 100 | |
与基线相比的变化。 | −1.6 | −2.8 | −0.4 | +0.1 | +2 |
与之前相比的变化。 | −1.6 | −1.6 | 0 | −0.1 | +1 |
结果 – CSS Wizardry:
FP | FCP | FWF | VC | 灯塔 | |
---|---|---|---|---|---|
1.7 | 2.2 | 4.9 | 5.0 | 99 | |
与基线相比的变化。 | −1.7 | −2.1 | +0.5 | +0.6 | +3 |
与之前相比的变化。 | −1.9 | −1.4 | +0.3 | +0.4 | +4 |
这些结果是巨大的。
我对这些结果真的很满意。首先,在我们的基线上,油漆的改进幅度达到了惊人的1.6-1.7秒,而在CSS Wizardry的情况下,比以前的变体改进了1.9秒。与我们的基线相比,第一次有内容的绘画改进了2.8秒,而Lighthouse的分数首次达到了100分。
就关键路径而言,这是一个巨大的胜利。
然而–这是个大_问题_–由于降低了CSS文件的优先级,在CSS Wizardry的情况下,我们的第一个网页字体与我们的基线相比慢了多达500ms。这就是印刷媒体黑客的危险之处。
异步加载谷歌字体是一个净好的主意,但是降低CSS文件的优先级实际上减缓了我们自定义字体的渲染速度。
我想说的是,异步CSS总体上是个好主意,但我需要以某种方式减轻优先级问题。
preload
好吧,如果打印CSS的优先级_太低_,我们需要的是一个高优先级的异步获取。输入preload
。
用一个preload
来补充我们的媒体技巧样式表,可以确保我们得到所有世界中最好的东西。
- 一个异步的高优先级获取,几乎可以在所有的现代浏览器中使用,并且。
- 一个非常广泛支持的方法,用于重新应用我们异步加载的CSS。
**注意:**我们不能完全采用preload
,因为它没有得到足够广泛的支持。事实上,在写这篇文章的时候,大约20%的网站访问者无法使用它。把打印样式表看作是一种退路。
片断。
<link rel="preload"
as="style"
href="$CSS&display=swap" />
<link rel="stylesheet"
href="$CSS&display=swap"
media="print" onload="this.media='all'" />
复制代码
**注意:**在未来,我们应该能够使用优先提示来解决这个问题。
结果 – harry.is:
FP | FCP | FWF | VC | 灯塔 | |
---|---|---|---|---|---|
1.8 | 1 .8 | 4.5 | 5.3 | 100 | |
与基线相比的变化。 | −1.6 | −2.8 | −0.4 | +0.3 | +2 |
与之前相比的变化。 | 0 | 0 | 0 | +0.2 | 0 |
结果 – CSS Wizardry:
FP | FCP | FWF | VC | 灯塔 | |
---|---|---|---|---|---|
2.0 | 2.0 | 4.3 | 4.3 | 98 | |
与基线相比的变化 | −1.4 | −2.3 | −0.1 | −0.1 | +2 |
与之前相比的变化 | +0.3 | −0.2 | −0.6 | −0.7 | −1 |
当第一种油漆不是保持不变就是变慢时,第一种内容丰富的油漆不是保持不变就是变快,而在CSS Wizardry的情况下,第一种网页字体比上一次迭代快了惊人的600ms。
在harry.is的情况下,与我们之前的变体相比,几乎没有任何变化。视觉上完整的速度快了200ms,但任何第一指标都没有改变。 正是看到这些结果,实际上刺激了我也对CSS Wizardry进行测试。因为harry.is是一个小而简单的页面,对于打印样式表来说,并没有太多的网络竞争,改变它的优先级并没有真正帮助它。
在CSS Wizardry的例子中,我们看到first paint慢了300ms,这是意料之外的,但与此无关(没有渲染阻塞CSS,所以改变一个异步CSS文件的优先级与此无关–我打算把它归结为测试中的一个异常)。令人高兴的是,第一个内容丰富的油漆提高了200毫秒,第一个网页字体提高了600毫秒,而视觉上的完整提高了700毫秒。
preload
ing Google Fonts是一个好主意。
preconnect
我想解决的最后一块难题是去另一个源头。虽然我们的CSS链接到了fonts.googleapis.com
,但字体文件本身是托管在fonts.gstatic.com
。在一个高延迟的连接上,这意味着坏消息。
preconnect
谷歌字体对我们很好–他们通过附在fonts.googleapis.com
响应上的HTTP头,预先将fonts.gstatic.com
。
虽然在这些演示中不是那么有效,但我希望有更多的第三方供应商能做这样的事情。
然而,这个头的执行受到响应的TTFB的约束,在高延迟网络上,TTFB可能非常非常高。在所有测试中,Google Fonts CSS文件的TTFB(包括请求排队、DNS、TCP、TLS和服务器时间)的中值是1406ms。相反,CSS文件的下载时间中值仅为9.5ms–获取文件头的时间比下载文件本身的时间长148倍。
fonts.gstatic.com
换句话说:即使谷歌为我们preconnect
,他们也只获得了大约10毫秒的领先优势。换个角度看,这个文件是受延迟限制的,而不是受带宽限制的。
如果我们实现一个第一方的preconnect
,我们应该可以获得一些相当大的收益。让我们看看会发生什么。
插图。
<link rel="preconnect"
href="https://fonts.gstatic.com"
crossorigin />
<link rel="preload"
as="style"
href="$CSS&display=swap" />
<link rel="stylesheet"
href="$CSS&display=swap"
media="print" onload="this.media='all'" />
复制代码
我们可以在WebPageTest中直观地看到这些好处。
看看我们可以通过preconnect
,将连接的开销提前多少。
结果 – harry.is:
FP | FCP | FWF | VC | 灯塔 | |
---|---|---|---|---|---|
1.8 | 1.8 | 3.8 | 4.4 | 100 | |
与基线相比的变化 | −1.6 | −2.8 | −1.1 | −0.6 | +2 |
与以前相比的变化 | 0 | 0 | −0.7 | −0.9 | 0 |
结果 – CSS Wizardry:
FP | FCP | FWF | VC | 灯塔 | |
---|---|---|---|---|---|
1.9 | 1.9 | 3.5 | 3.6 | 99 | |
与基线相比的变化 | −1.5 | −2.4 | −0.9 | −0.8 | +3 |
与之前相比的变化 | −0.1 | −0.1 | −0.8 | −0.7 | +1 |
我们来了!首先(内容丰富)的油漆现实上没有被触动。这里的任何变化都与我们的preconnect
,因为preconnect
只影响关键路径之后的资源。我们的重点是第一个网页字体和视觉上的完成,这两个方面都有巨大的改进。与我们之前的变体相比,第一种网页字体有700-1200毫秒的改进,视觉完成有700-900毫秒的改进,而与我们的基线相比,分别有600-900毫秒和600-800毫秒的改进。Lighthouse的得分在100和99之间。
preconnect
ingfonts.gstatic.com
是一个好主意。
奖金。font-display: optional;
使用异步CSS和font-display
,我们很容易受到FOUT的影响(或者,如果我们正确设计了Fallbacks,则希望是FOFT)。为了尝试减轻这种情况,我决定使用font-display: optional;
进行测试。
这个变体告诉浏览器,网页字体被认为是可选的,如果我们在极小的封锁期内无法获得字体文件,那么我们就不提供交换期。这样做的实际效果是,如果网页字体的加载时间过长,该页面就根本不会使用它。这有助于防止FOUT,这反过来会给你的用户带来更稳定的体验–他们不会在浏览页面的中途看到文本重新设计,而且会有更好的累计布局转移得分。
然而,在使用异步CSS时,这被证明一直是个麻烦。当print
样式表被转化为all
样式表时,浏览器会更新CSSOM,然后将其应用于DOM中。在这一时刻,页面被告知需要一些网页字体,极小的块状周期开始启动,在页面加载生命周期的中途显示一个FOIT。更糟糕的是,浏览器会用它开始时的回退来替换FOIT,所以用户甚至没有得到新字体的好处。这基本上看起来像一个错误。
把它形象化比解释要容易得多,所以这里有一张胶片的截图。
注意FOIT分别在3.4-3.5s和3.2s。
还有一段在DevTools中显示这个问题的视频。
我不建议将font-disply: optional;
与异步CSS一起使用;我建议使用异步CSS。Ergo,我不建议使用font-display: optional;
。总的来说,拥有非阻塞的CSS与FOUT比拥有不必要的FOIT要好。
比较和视觉化
在这些慢动作视频中,你可以很清楚地看到两者的区别。
harry.is
- Async,
preload
, 和preconnect
都在1.8s开始渲染。- 这也代表了他们的第一次内容丰富的油漆–在第一次渲染中的有用信息。
- Legacy和
swap
都在3.4秒时开始渲染。- 虽然遗留问题缺少任何文字-FOIT。
preconnect
在3.8秒时加载其网页字体。- 它在4.4s时被认为是视觉上完整的。
- Legacy在4.5秒内完成了它的第一个内容和第一个网络字体的绘制。
- 它们是一体的,因为一切都在同步进行。
- Legacy的视觉完成时间是5s。
- 非同步的视觉完成时间为5.1秒。
swap
在5.2秒时被视为完成。preload
在5.3秒时被视为视觉上完成。
CSS向导
- Async在1.7s时开始渲染。
preconnect
在1.9s时开始渲染。- 它的第一次有内容的绘制也是1.9s–在第一次渲染中的有用信息。
preload
在2s时开始渲染。- 它的第一次有内容的绘制也是在2s。
- Async在2.2s时渲染内容。
- Legacy在3.4s时开始渲染。
swap
在3.6s时进行第一次和第一次有内容的渲染。preconnect
在3.6s时被认为是视觉上完成。- Legacy在4.3s时进行了第一次有内容的绘制。
preload
在4.3s时视觉上完成。- Legacy在4.4s时被认为视觉上完成。
swap
在4.6s时完成。- Async以5秒的时间排在最后。
在所有指标中,preconnect
是最快的。
调查结果
虽然自我托管你的网页字体可能是解决性能和可用性问题的总体最佳方案,但我们能够设计一些相当有弹性的措施,以帮助在使用谷歌字体时减轻很多这些问题。
异步加载CSS、异步加载字体文件、选择进入FOFT、快速获取异步CSS文件以及预热外部域的组合,使得体验比基线快几秒。
如果你是谷歌字体的用户,我强烈建议你采用这个方法。
如果Google Fonts不是你唯一的渲染阻塞资源,如果你违反了快速CSS的其他原则(例如,如果你在@import
ing你的Google Fonts CSS文件),那么你的里程将有所不同。 这些优化对Google Fonts构成最大性能瓶颈的项目最为有利。
谷歌字体的异步片段
这里结合了很多技术,但所产生的代码仍然很薄,可维护性也很强,应该不会构成问题。这个片段不需要拆开,可以全部放在你的文档中的<head>
。
这里是用于快速谷歌字体的最佳片段。
<!--
- 1. Preemptively warm up the fonts’ origin.
-
- 2. Initiate a high-priority, asynchronous fetch for the CSS file. Works in
- most modern browsers.
-
- 3. Initiate a low-priority, asynchronous fetch that gets applied to the page
- only after it’s arrived. Works in all browsers with JavaScript enabled.
-
- 4. In the unlikely event that a visitor has intentionally disabled
- JavaScript, fall back to the original method. The good news is that,
- although this is a render-blocking request, it can still make use of the
- preconnect which makes it marginally faster than the default.
-->
<!-- [1] -->
<link rel="preconnect"
href="https://fonts.gstatic.com"
crossorigin />
<!-- [2] -->
<link rel="preload"
as="style"
href="$CSS&display=swap" />
<!-- [3] -->
<link rel="stylesheet"
href="$CSS&display=swap"
media="print" onload="this.media='all'" />
<!-- [4] -->
<noscript>
<link rel="stylesheet"
href="$CSS&display=swap" />
</noscript>
复制代码