如何优雅地加载Fonts

开发 前端
选择一些看起来不错的网络可用字体,获取 HTML 或 CSS 代码段,将其放入项目中,然后检查它们是否显示正确。

使用网络字体的工作流很简单,是吧?选择一些看起来不错的网络可用字体,获取 HTML 或 CSS 代码段,将其放入项目中,然后检查它们是否显示正确。人们每天数以千次地像这样操作谷歌字体:将它的<link>标签放入<head>中。

我们来看下Lighthouse是如何评价这样的工作流的。

Lighthouse

标签中的样式表被Lighthouse标记为阻塞渲染的资源,它们竟然让渲染增加了一秒钟?这看起来不是很好。

我们已经按照书籍,文档,HTML标准完成了所有工作,为什么Lighthouse还是会告诉我这样不对呢?

让我们来讨论下如何让字体样式文件成为非渲染阻塞资源,并且探讨一种不仅让Lighthouse满意,还要解决加载字体时通常会出现糟糕的无样式文本闪烁(FOUT)问题。我们将使用原生的HTML,CSS和JavaScript来完成这些工作,因此我们可以在任何技术栈中使用它们。另外我们还将介绍下Gatsby的实现以及一个由本人开发的简单、开箱即用的插件。

什么是渲染阻塞字体

浏览器加载网页时,会从DOM(HTML的一个对象模型)和CSSOM(所有css选择器的映射)中生成渲染树。渲染树是关键渲染路径中的一部分,关键渲染路径代表了浏览器在渲染页面中的每个步骤。浏览器为了渲染一个页面,需要加载并解析HTML文档以及链接到该HTML中的每个CSS文件。

下面是一个非常典型的直接从谷歌字体样式表: 

  1. @font-face {  
  2.   font-family: 'Merriweather';  
  3.   src: local('Merriweather'), url(https://fonts.gstatic.com/...) format('woff2');  

你可能会认为字体样式的文件体积很小,因为他们通常最多包含一些@font-face定义。因此它们应该不会对页面渲染造成可以感知的影响,事实是这样吗?

假设我们正在从外部CDN服务器加载一个CSS字体文件。当我们的网页在加载时,浏览器需要等待文件从CDN服务器上加载并且载入到渲染树中。不仅如此,它还需要等待在CSS的@font-face属性中定义的作为URL值引用的字体文件请求并加载完成。

关键点:字体文件成为关键渲染路径的一部分,并且延迟了页面的渲染。

加载字体样式表和字体文件时阻塞了关键渲染路径

对于普通用户来说网站最重要的部分是什么?当然是内容了。因此在页面加载过程中我们必须尽快将内容呈现给用户。为此,关键渲染路径必须精简到只剩下关键资源(例如HTML和关键的CSS),其余的内容将在页面渲染完成后加载,包括字体。

如果一个用户正在较差的网络环境下浏览未经过优化的页面,坐在空白的屏幕前等待字体文件和其他关键资源载入完成将会使他非常恼火。除非该用户非常有耐心,否则他很可能会以为是页面根本没有开始加载,直接放弃等待,关闭窗口。

然而,如果非关键资源的渲染被推迟,内容尽可能快地被呈现了出来,用户将能够浏览页面并且忽略任何缺少的表现样式(比如字体)——当然,如果字体并不是内容的一部分的话。

 

经过优化的网站尽可能快地渲染包含关键CSS的内容,并且延迟非关键资源的加载。第二个时间线的0.5s和1.0s之间出现了一个字体的切换,表明了显示样式开始渲染的时间。

载入字体的最佳实践

重复造轮子没有意义。哈里·罗伯茨(Harry Roberts)已经很好地介绍了一种加载字体的最佳实践。他通过深入的研究以及来自谷歌字体的数据,将其归纳为四个步骤:

  •  预连接到字体文件的站点。
  •  以低优先级异步预加载字体样式表。
  •  在内容渲染完成后,使用JavaScript异步加载字体样式表和字体文件。
  •  为关闭JavaScript的用户提供一个备用字体。

让我们使用Harry的方法来实现我们的字体: 

  1. <!-- https://fonts.gstatic.com is the font file origin -->  
  2. <!-- It may not have the same origin as the CSS file (https://fonts.googleapis.com) -->  
  3. <link rel="preconnect"  
  4.       href="https://fonts.gstatic.com"  
  5.       crossorigin />  
  6. <!-- We use the full link to the CSS file in the rest of the tags -->  
  7. <link rel="preload"  
  8.       as="style"  
  9.       href="https://fonts.googleapis.com/css2?family=Merriweather&display=swap" />  
  10. <link rel="stylesheet"  
  11.       href="https://fonts.googleapis.com/css2?family=Merriweather&display=swap"  
  12.       media="print" onload="this.media='all'" />  
  13. <noscript>  
  14.   <link rel="stylesheet"  
  15.         href="https://fonts.googleapis.com/css2?family=Merriweather&display=swap" />  
  16. </noscript> 

注意字体样式链接上的media="print"。浏览器会自动赋予打印样式一个低优先级并将其排除在关键渲染路径之外。在打印样式表加载完成后,onload事件将会被触发,媒体将会切换为一个默认的all值,字体将被应用于所有的媒体类型(屏幕、打印和语音)。

Lighthouse很满意这个方法!

需要注意的是,自托管字体还可以帮助解决渲染阻塞问题,但这并非适用于所有情况。例如,使用CDN可能是不可避免的。在某些情况下,让CDN在提供静态资源时承担繁重的工作是有益的。

尽管我们现在以最佳的非渲染阻止的方式加载了字体样式表和字体文件,我们还是引入了一个小的用户体验问题……

无样式文本闪烁(FOUT)

这就是我们说的FOUT:

为什么会这样呢?为了去掉阻塞渲染的资源,我们必须在页面内容已渲染(即显示在屏幕上)之后加载它。对于在关键资源之后异步加载的低优先级字体样式表,用户可能会看到从备用字体切换为下载完的字体的瞬间。不仅如此,页面的布局可能会偏移,导致一些元素在网络字体加载完成之前看起来很割裂。

处理FOUT的最好方式是让备用字体和网络字体之间的切换平滑。为此我们要做的有:

  •  尽可能选择一个与异步加载的字体相似的系统字体。
  •  调整备用字体的样式(font-size,line-height,letter-spacing等),尽可能地让异步载入的字体与备用字体特征匹配。
  •  在异步加载的字体文件渲染完成后立即清除备用字体的样式,并应用为新载入的字体准备的样式风格。

我们可以使用字体匹配器找到我们选定并计划使用的任何网络字体的最佳备用字体和配置。在我们为备用字体和网络字体准备好样式风格后,我们可以继续下一步。

本例中Merriweather是要使用的字体,Georgia是备用字体。一旦Merriweather的样式风格被应用,布局偏移应当降到最小,并且字体切换不应该被察觉。

我们可以使用CSS字体加载API来检测Web字体何时加载。为什么要这样?Typekit的网络字体加载器曾经是实现它的一种比较流行的方法,尽管继续使用它或其他类似的库看起来很诱人,但我们需要考虑以下几点:

  •  它已经四年没有更新了,这意味着,如果插件有任何问题或需要引入新功能,很可能没有人会更新与维护了。
  •  我们已经使用Harry Roberts的代码有效地处理了异步加载,不需要依赖JavaScript来加载字体。

如果你问我,使用类似Typekit的库为这样的简单任务带来了过多的JavaScript代码。我想避免使用任何第三方的库和依赖,因此让我们自己找到解决方案,并且尽可能地使其简洁明了,而不会过度设计。

尽管CSS字体加载API被认为是实验性功能,但它已经有大约95%的浏览器支持了。但无论如何,我们还是要提供备用字体,因为API可能在将来发生了改变或被废弃。丢失字体的风险不值得我们去冒。

CSS字体加载API可用于动态和异步地加载字体。我们已经决定不依赖JavaScript来完成诸如字体加载之类的简单操作,并且已经使用带有预加载和预连接的纯HTML的最佳实践解决了它。我们将使用API中的一个函数,该函数将帮助我们检查字体是否已加载并可用。 

  1. document.fonts.check("12px 'Merriweather'"); 

check()函数判断在参数中指定的字体是否可用并返回true或false。字体大小参数值对于我们的用例而言并不重要,可以将其设置为任何值。不过,我们需要确保:

  •  页面上至少有一个HTML元素,其中包含至少一个字符,并已应用了网络字体的声明。在示例中,我们将使用&nbsp;。但任何字符都可以使用,只要它对于有视力和无视力的用户都是隐藏的(不使用display: none;)。API跟踪已应用字体样式的DOM元素。如果页面上没有匹配的元素,则API无法确定字体是否已加载。
  •  在check()函数的参数中指定的字体正是CSS中调用的字体。

在下面的演示中,我已经使用CSS字体加载API实现了字体加载的监听器。出于演示的目的,加载字体及其监听器将通过点击模拟页面加载的按钮来触发,以便您可以看到改变的发生。在常规项目中,这应该在网站加载和渲染完成后立即进行。

那不是很棒吗?多亏了CSS字体加载API的良好支持,们花了不到30行JavaScript代码来实现一个简单的字体加载监听器。在此过程中,我们还处理了两种可能的极端情况:

  •  API出了点问题,或者发生了一些阻止网络字体加载的错误。
  •  用户正在关闭JavaScript功能的情况下浏览网站。

现在,我们有了一种检测字体文件何时完成加载的方法,我们需要向备用字体中添加样式以匹配网络字体,并了解如何更有效地处理FOUT。

备用字体和网络字体之间的切换看起来很平滑,我们设法实现了不那么明显的FOUT!在复杂的站点上,这个改变将会带来少量的布局偏移,依赖内容大小的元素也不会看起来被破坏或者位置不对。

背后发生了什么

让我们从HTML开始仔细看一下前面示例中的代码。我们有<head>元素中的代码片段,它使我们可以通过预加载、预连接和存在备用地异步载入字体。 

  1. <body class="no-js">  
  2.   <!-- ... Website content ... -->  
  3.   <div aria-visibility="hidden" class="hidden" style="font-family: '[web-font-name]'">  
  4.       /* There is a non-breaking space here */  
  5.   </div>  
  6.   <script>   
  7.     document.getElementsByTagName("body")[0].classList.remove("no-js");  
  8.   </script>  
  9. </body> 

注意我们在<body>元素上有一个硬编码的.no-js类名,它将在HTML文档完成加载的时刻被移除。这会为禁用JavaScript的用户呈现网络字体的样式。

其次,还记得CSS字体加载API是如何要求最少包含一个字符的HTML元素来追踪字体并应用它的样式的吗?由于我们无法使用display: none;,我们添加了一个包含&nbsp字符的<div>,以一种有效的方式实现了对有视力和无视力的用户的隐藏。这个元素有一个内联样式font-family: 'Merriweather'。这使我们能够在备用的样式和加载完成的字体样式之间进行平滑切换,并确保所有字体文件被正确追踪,无论他们是否在页面上使用。

注意&nbsp;字符并没有在代码块中出现但它确实存在!

CSS是最简单的部分。我们可以利用硬编码在HTML中的CSS类名或有条件地使用JavaScript处理不同字体的加载状态。 

  1. body:not(.wf-merriweather--loaded):not(.no-js) {  
  2.   font-family: [fallback-system-font];  
  3.   /* Fallback font styles */  
  4.  
  5. .wf-merriweather--loaded, 
  6.  .no-js {  
  7.   font-family: "[web-font-name]";  
  8.   /* Webfont styles */  
  9.  
  10. /* Accessible hiding */  
  11. .hidden {  
  12.   position: absolute;   
  13.   overflow: hidden;   
  14.   clip: rect(0 0 0 0);   
  15.   height: 1px;  
  16.   width: 1px;   
  17.   margin: -1px;  
  18.   padding: 0;  
  19.   border: 0;   

JavaScript是神奇的地方。如前所述,我们使用CSS字体加载API的check()函数检查字体是否被成功加载。同样,字体大小的参数可以是任何值(以像素为单位);它的font-family属性值需要与我们载入的字体名称相同。 

  1. var interval = null 
  2. function fontLoadListener() {  
  3.   var hasLoaded = false 
  4.   try {  
  5.     hasLoaded = document.fonts.check('12px "[web-font-name]"')  
  6.   } catch(error) {  
  7.     console.info("CSS font loading API error", error);  
  8.     fontLoadedSuccess();  
  9.     return; 
  10.   } 
  11.   if(hasLoaded) { 
  12.     fontLoadedSuccess();  
  13.   }  
  14.   
  15. function fontLoadedSuccess() {  
  16.   if(interval) {  
  17.     clearInterval(interval);  
  18.   }  
  19.   /* Apply class names */  
  20.  
  21. interval = setInterval(fontLoadListener, 500); 

这段代码是我们在使用fontLoadListener()设置定期运行的监听器。这个函数应该尽可能简单,以便在定时器间隔内有效运行。我们使用try-catch代码块来处理任何错误并捕获任何问题,以便在JavaScript错误的情况下仍然可以使用网络字体样式,使用户不会遇到任何界面显示的问题。

接下来,我们使用fontLoadedSuccess()监听何时字体加载完成。我们需要确保立即清除定时器以免在此之后进行不必要的字体加载检查。为了应用网络字体的风格样式,我们可以在这里添加一些需要的类名。

最后,我们初始化定时器的周期。在此示例中,我们将其设置为500ms,因此该函数每秒运行两次。

这是Gatsby的实现

与一般的Web开发(甚至是常规的create-react-app技术栈)相比,Gatsby所做的事情有所不同,这使得实现此处介绍的内容有些棘手。

为了简化这一过程,我们将开发一个本地的Gatsby插件,因此在下面的示例中所有与字体加载器相关的代码都位于plugins/gatsby-font-loader。

我们的字体加载器代码和配置将分为三个主要的Gatsby文件:

  •  插件配置(gatsby-config.js):我们将引入项目中的本地插件,列出所有的本地和外部字体及其属性(包括字体名称和CSS文件URL),并引入所有预连接URL。
  •  服务器端代码(gatsby-ssr.js):我们将使用来自Gatsby API的setHeadComponents 函数根据配置生成并引入HTML<head>中预加载和预连接的标签。然后,我们使用setPostBodyComponents生成隐藏了字体的HTML代码并将其引入HTML文档中。
  •  客户端代码(gatsby-browser.js):由于此代码在页面加载完成以及React启动后运行,因此它已经是异步的。这意味着我们可以使用react-helmet注入字体样式表链接。我们还将启动字体加载监听器来处理FOUT。

您可以在下面的CodeSandbox示例中检查Gatsby实现。 

  1. <iframe   
  2. allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking"  
  3. sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts allow-downloads"   
  4. width="700px" height="500px"   
  5. frameborder="0" scrolling="no"  
  6. src="https://g21lp.sse.codesandbox.io"   
  7. style="opacity: 1; z-index: 1;   
  8. background-color: white; pointer-events: initial;">   
  9. </iframe> 

我知道,有些东西很复杂。如果你只想为实现性能优化,异步加载字体和避免FOUT问题使用简单的开箱即用的解决方案,我已经为此开发了gatsby-omni-font-loader插件。它使用本文中的代码,我正在积极维护它。如果您有任何建议,错误报告或代码贡献,请随时在GitHub上提交。

结论

内容可能是用户在网站上体验的最重要组成部分。我们需要确保内容获得最高优先级并尽快加载。这意味着在加载过程中尽量减少使用最小表示样式(即,内联的关键CSS)。这也就是为什么在大多数情况下将网络字体视为非关键资源的原因——用户仍然可以在没有字体的情况下查看内容——因此,在页面渲染完成之后加载它们非常好。

但这可能会导致FOUT和布局偏移,因此我们需要字体加载监听器来保证备用的系统字体和网络字体之间进行平滑切换。

我想听听你的想法!在评论中让我知道您如何处理项目中网络字体加载,渲染阻塞的资源和FOUT的问题。 

 

责任编辑:庞桂玉 来源: 前端大全
相关推荐

2022-04-20 20:27:51

Hydra配置文件开发工具

2024-11-13 16:37:00

Java线程池

2020-03-26 11:04:00

Linux命令光标

2021-05-12 22:07:43

并发编排任务

2021-01-18 13:17:04

鸿蒙HarmonyOSAPP

2022-05-13 21:20:23

组件库样式选择器

2021-01-28 14:53:19

PHP编码开发

2022-05-24 06:07:48

JShack用户代码

2024-04-24 12:34:08

Spring事务编程

2021-09-08 08:34:37

Go 文档Goland

2020-10-22 10:15:33

优化Windows电脑

2018-08-20 10:40:09

Redis位图操作

2020-12-08 08:08:51

Java接口数据

2023-02-13 14:37:13

开发web浏览器

2020-11-06 08:13:03

服务器Nodejs客户端

2020-09-25 11:30:20

Java判空代码

2020-07-09 10:15:55

空值Bug语言

2021-03-26 20:37:14

Prometheus监控指标

2017-12-14 14:17:08

Windows使用技巧手册

2020-02-05 14:05:21

Java技术数组
点赞
收藏

51CTO技术栈公众号