一般来说,今天的Web字体速度比以往任何时候都更快。浏览器厂商在进一步规范FOUT/FOIT行为,新一代font-display规范也开始普及,看来性能(和用户体验)终于成为了人们关注的焦点。
本文最初发布于csswizardry.com网站,经原作者授权由InfoQ中文站翻译并分享。
FOUT:无样式文本闪现,Flash of Unstyled Text FOIT:不可见文本闪现,Flash of Invisibale Text FOFT:伪文本闪现,Flash of Faux Text
人们一般认为自托管字体是最快的选项:相同来源意味着较少的网络协商,可预测的URL意味着我们可以preload(预加载),自托管意味着我们可以设置自己的cache-control(缓存控制)指令(https://csswizardry.com/2019/03/cache-control-for-civilians/),而完整所有权可以减少将静态资产留在第三方源上的风险(https://csswizardry.com/2019/05/self-host-your-static-assets/)。
然而,像谷歌字体(Google Fonts)这种服务的便利性也是不可小觑的。他们能够为特定用户代理和平台量身打造体积最小巧的字体文件,再加上由谷歌CDN网络分发的规模庞大的免费库等优势,难怪越来越多的用户在转向谷歌字体了。
本网站(csswizardry)极度重视性能表现,所以我完全放弃了Web字体,而选择使用访客的系统字体。这种方案速度飞快,可以适应各种各样的设备平台,并且工程开销几乎为零。但在harry.is这个网站上,我很想抛弃理性,随心所欲一次。面对恐惧吧!这个网站上我要用Web字体!
然而,我还是不能牺牲性能。
在网站的原型设计阶段,我转向了谷歌字体。它们最近添加了通过一个URL参数(&display=swap)支持font-display的功能,所以速度应该是很快的。然后我有了一个主意。
首先看这条推文:
虽然font-display: swap效果出众,但还有很多事情都不能让我满意。我能再做些什么来进一步提升谷歌字体的性能呢?
结果,我给自己挖了个大坑……
我针对harry.is和csswizardry.com主页运行了相同的测试套件。首先测试的是harry.is,因为我在这个网站上使用了谷歌字体,但它的页面太简单了,没什么代表性。因此我把CSS Wizardry的主页复制了几份,分别实现了几种不同的方法。本文的每个小节中我都会列出两个站点的测试结果。我的方法有:
此外,每种方法都是上一种方法的扩展——也就是在上一种方法的基础上加入新内容。我不会只尝试preload或异步,因为这样做毫无意义——我们知道,各种选项结合起来使用才会有最好的效果。
每次测试时,我会捕获以下指标:
注意:所有测试都是使用一个私有WebPageTest实例进行的(WebPageTest现在下线了,因此我无法使用公共实例,也就是说我无法共享任何URL,抱歉)。具体的配置文件是Samsung Galaxy S4 over 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 @规则。参数的值可以是swap、optional、fallback或block。MDN上有更多信息(https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display)。
但我的基线会关闭font-display。许多站点可能还在使用这种旧格式,所以用它做基线比较合适。
代码段:
<link rel="stylesheet"
href="$CSS" />
这里有两个关键问题:
这是同步中的同步,不是什么好事。
上面就是我们的基线成绩。谷歌字体文件是两个测试中唯一阻塞渲染的外部资源,并且我们可以看到两个测试的first paint结果完全一致。在本文这些实验的过程中,让我很惊讶的一件事就是谷歌字体的一致性表现。
这里,Lighthouse给出了一个错误和一个警告:
前者是因为没有Web字体加载方案(例如font-display);后者是因为同步的谷歌字体CSS文件。
现在我们一点点加入改动,希望能获得一些改进。
这一节中我加回来了&display=swap。它的效果是让字体文件本身变成异步的——浏览器会先显示我们的回退(fallback)文本,等Web字体可用时再切换过去。也就是说我们不会让用户看到任何不可见的文本(FOIT),从而带来更快、更舒适的体验。
注意:如果你能精心定义一个在过渡期间显示的合适回退,效果会更好——如果Open Sans字体就绪之前,用户看到满页面都是Times New Roman字体,这种体验就会很糟糕了。还好我们有Monica这个工具(https://twitter.com/notwaldorf),让这里的工作变得轻松愉快。她的她的字体样式匹配器(https://meowni.ca/font-style-matcher/)非常好用
代码段:
<link rel="stylesheet"
href="$CSS&display=swap" />
我们没有从关键路径中移除任何阻塞渲染的资源,所以first paint应该不会有任何改善。实际上,虽然harry.is的成绩没变,但CSS Wizardry的速度慢了200毫秒。但与此同时,我们看到first contentful paint成绩显著提升,在harry.is上提升了超过1秒!在harry.is上first web font的成绩进步了,但在csswizardry.com上却没有。Visually complete速度慢了200毫秒。
我很高兴能看到最关键的几个指标快了700–1200毫秒。
尽管这种方法确实可以大大改善渲染Web字体的速度,但它仍然是在一个同步CSS文件中定义的——这一步能带来的改进也就这么多了。
预料之中,Lighthouse现在只发出一个警告:
那么下一步就是解决同步CSS文件的问题。
引用一下我自己的话:"如果你要在谷歌字体中使用font-display,那么就应该异步加载整个请求链。"如果我能让CSS文件的内容异步处理,那么还让CSS文件本身完全同步就显得很傻了。
font-display: swap;是个好主意。
引入Critical CSS时,一项关键技术就是让CSS异步处理。要做到这一点可用的方法很多,但我敢说,最简单,最常用的就是Filament Group的print media type技巧(https://www.filamentgroup.com/lab/load-css-simpler/)。
这种方法将隐式告诉浏览器以非阻塞方式加载CSS文件,仅将样式应用于print上下文。但当文件传输过来时,我们会告诉浏览器将其应用于all上下文,为页面的剩余部分应用样式。
代码段:
<link rel="stylesheet"
href="$CSS&display=swap"
media="print" onload="this.media='all'" />
尽管这个技巧非常简单,但我长期以来一直对它持保留态度。可以看到,常规的同步样式表会阻塞渲染,因此浏览器会为其分配“最高”优先级。但print样式表(或任何与当前上下文不匹配的样式表)分配的优先级就完全相反了:Idle。 也就是说,在浏览器开始发出请求时,异步CSS文件往往会以非常低的优先级来处理(或者说,它的优先级是正确的,但比你想的低很多)。以我的一个客户Vitamix为例,我为他们实现了异步CSS,字体提供商是他们自己的:
虽然Chrome可以执行异步DNS/TCP/TLS,但它会在较慢的连接上序列化并停止非关键请求。
浏览器完全按照我们的指示做事:要求这些CSS文件具有print样式表该有的优先级。因此在3G网络连接上,要花费超过9秒才能下载完各个文件。浏览器将其他几乎所有内容(包括body内的资源)都放在了print样式表前面。也就是说页面在3G网络上要等足足12.8秒才能渲染好第一个自定义字体。
还好在使用Web字体时,这个问题并不是无解的:
但对于fold CSS中的内容来说,将近10秒的延迟是不可接受的——用户肯定不会等这么久,早就去看其他内容了。
如果我们异步加载谷歌字体,会发生什么情况呢?
结果非常出色。
我对这些结果感到非常满意。与我们的基线相比,first paint大幅改善了1.6-1.7秒,而CSS Wizardry上则比上一节中提升了1.9秒。First contentful paint改进了2.8秒之多,并且Lighthouse分数首次达到100。
就关键路径而言,这是一个巨大的胜利。
但是——这个"但是"不可忽视——由于CSS文件的优先级降低了,CSS Wizarddry的first web font比我们的基线慢了500毫秒之多。这就是代价所在。
异步加载谷歌字体是一个不错的主意,但是CSS文件的优先级下降实际上减慢了自定义字体的渲染速度。
我要说的是,异步CSS总体来说是个好主意,但我需要以某种方式缓解优先级问题。
如果print CSS的优先级太低,我们需要的就是高优先级的异步提取(fetch)。该轮到preload上场了。
在上一节方法的基础上加入preload后,我们就能两全其美了:
注意:我们不能完全依赖preload,因为它还没有获得足够的支持。实际上在撰写本文时,大约有20%的网站用户无法使用它。可以考虑将print样式表作为回退。
代码段:
<link rel="preload"
as="style"
href="$CSS&display=swap" />
<link rel="stylesheet"
href="$CSS&display=swap"
media="print" onload="this.media='all'" />
注意:将来,我们应该可以使用优先级提示(Priority Hints)来解决这个问题。
虽然first paint的成绩没变或变慢了,但first contentful paint的成绩则变快了或至少没变;在CSS Wizardry的测试中,first web font比上一个迭代的速度快了600毫秒。
在harry.is的测试中各项指标和上一节相比没什么变化。Visually complete成绩提高了200毫秒,但所有first-指标均未受影响。由于harry.is的页面非常小巧,因此print样式表没什么负面影响——其优先级的变化并不会有什么效果可言。
在CSS Wizardry的测试中,我们意外地看到first paint要慢300毫秒,不过这也无关紧要(这里没有阻塞渲染的CSS,因此更改异步CSS文件的优先级可能没有任何影响——我就当它是测试中的异常了)。让人高兴的是,first contentful paint的成绩提升了200毫秒,first web font的速度提高了600毫秒,而visually complete的速度提高了700毫秒。
preload谷歌字体是一个好主意。
接下来是最后一个问题。当我们为CSS链接到fonts.googleapis.com时,字体文件本身却托管在fonts.gstatic.com上。遇到高延迟连接时这就是大问题了。
谷歌字体很贴心——它们会通过附在fonts.googleapis.com响应上的一个HTTP标头抢先preconnect fonts.gstatic.com这个源:
尽管在这些演示中效果不尽如人意,但我希望更多的第三方提供商可以做到这一点。
但是,这个标头的执行受到响应的TTFB(Time to First Byte)时间约束,在高延迟网络中TTFB可能会非常高。在所有测试中,谷歌字体CSS文件的TTFB(包括请求队列、DNS、TCP、TLS和服务器时间)的中位数为1406毫秒。相比之下,CSS文件的平均下载时间仅为9.5毫秒——到达文件标头所花费的时间是下载文件本身所花费时长的148倍。
换句话说,即使谷歌为我们preconnect了fonts.gstatic.com源,他们也只能获得10毫秒的优势。也就是说这个文件遇到了延迟瓶颈,而不是带宽瓶颈(https://csswizardry.com/2019/01/bandwidth-or-latency-when-to-optimise-which/)。
如果我们实现了第一方的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前移了多少连接开销。
First(contentful)paint的成绩其实没受影响。此处的任何更改都与我们的preconnect无关,因为preconnect仅影响关键路径之后的资源。我们关注的是first web font和visually complete的成绩,这两个指标都显示出巨大的进步。与我们之前的版本相比,first web font的改进为700–1,200毫秒,visually complete的改进为700–900毫秒;与我们的基线相比则分别为600–900毫秒和600–800毫秒。Lighthouse分数分别是100和99。
preconnect fonts.gstatic.com是一个好主意。
使用异步CSS和font-display会使我们容易受到FOUT的影响(或者如果我们正确设计了回退,就会是FOFT)。为了缓解这种问题,我决定使用font-display: optional;做一次测试。
这种方法会告诉浏览器:Web字体被认为是可选的,如果在"极短的阻塞时间"内我们无法获取字体文件,则我们"不提供切换期"。实际的效果是,如果Web字体加载时间太长,那么页面视图就完全不会用它。这样就能避免FOUT,从而为用户带来更稳定的体验——他们不会在页面浏览的途中看到文本样式出现变化——以及更好的累积布局偏移(Cumulative Layout Shift)分数。
但事实证明,在使用异步CSS时这个方法总是很麻烦。当print样式表变成all样式表时,浏览器将更新CSSOM,然后将其应用于DOM。此刻,页面被告知它需要一些Web字体,进入"极短的阻塞时间",在页面加载生命周期中显示一个FOIT。更糟糕的是,浏览器将用和一开始时一样的回退来替代FOIT,因此用户甚至无法体验新字体的好处。它看起来就像是一个错误。
一图胜千言,以下是整个过程的屏幕截图:
注意3.4-3.5秒和3.2秒时的FOIT
DevTools中关于这个问题的视频:
https://csswizardry.com/wp-content/uploads/2020/05/video-devtools-foit.mp4
我不建议同时使用font-disply: optional;和异步CSS。总的来说,FOUT加上非阻塞CSS总比没必要的FOIT要强。
在下面这些慢动作视频中,你可以清楚地看到不同方法之间的差异。
https://csswizardry.com/wp-content/uploads/2020/05/video-comparison-harry.is.mp4
https://csswizardry.com/wp-content/uploads/2020/05/video-comparison-csswizardry.com.mp4
preconnect的所有指标都是最快的。
尽管自托管Web字体可能是解决性能和可用性问题的最佳方法,但我们可以设计一些相当灵活的措施来缓解使用谷歌字体时出现的许多相关问题。
异步加载CSS、异步加载字体文件、选择FOFT、快速提取异步CSS文件以及预热外部域等方法的组合,能获得比基线快几秒钟的成绩。
如果你是谷歌字体用户,强烈建议你采用这套方法。
如果谷歌字体不是你唯一的渲染阻塞资源,并且你违反了其他快速CSS原则(例如@import导入谷歌字体CSS文件),结果还会有差异。如果谷歌字体是你最大的性能瓶颈,那么这些优化措施的效果就会非常明显了。
本文介绍了很多技巧,但是它们结合起来生成的代码仍然很轻巧且容易维护,不会带来问题。这一段代码无需拆分,可以全部保存在文档的<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>
原文链接:
领取专属 10元无门槛券
私享最新 技术干货