如何优雅地加载字体
时间:2023-03-15 13:46:31
科技观察
使用网络字体是一个简单的工作流程,不是吗?选择一些看起来不错的web-ready字体,抓取HTML或CSS片段,将它们放入您的项目中,并检查它们是否正确显示。人们每天都会像这样使用GoogleFonts数千次:将其标记放在
中。让我们看看Lighthouse如何评估这样的工作流程。Lighthouse标签中的样式表被Lighthouse标记为渲染阻塞资源,他们给渲染加了秒?这看起来不太好。我们已经按照书、文档、HTML标准做了一切,为什么Lighthouse仍然告诉我这是错误的?让我们来讨论一下如何让字体样式文件成为非渲染阻塞资源,探索一种既能让Lighthouse开心,又能解决加载字体时经常出现的可怕的无样式文本闪烁(FOUT)问题的方法。我们将使用本机HTML、CSS和JavaScript来执行此操作,因此我们可以在任何技术堆栈中使用它们。此外,我们还将介绍Gatsby的实现以及我自己开发的一个简单、开箱即用的插件。什么是渲染阻塞字体?当浏览器加载网页时,它会从DOM(HTML的对象模型)和CSSOM(所有css选择器的映射)生成渲染树。渲染树是关键渲染路径的一部分,它代表浏览器在渲染页面时所采取的每个步骤。为了让浏览器呈现页面,它需要加载和解析HTML文档以及链接到该HTML的每个CSS文件。这是直接来自Google字体的非常典型的样式表:@font-face{font-family:'Merriweather';src:local('Merriweather'),url(https://fonts.gstatic.com/…)format('woff2');}您可能认为字体样式文件很小,因为它们通常最多包含几个@font-face定义。所以它们应该对页面呈现没有明显的影响,是这样吗?假设我们正在从外部CDN服务器加载CSS字体文件。我们的网页在加载的时候,浏览器需要等待从CDN服务器加载文件,加载到渲染树中。不仅如此,它还需要等待CSS的@font-face属性中引用的字体文件被请求加载。要点:字体文件成为关键渲染路径的一部分并延迟页面渲染。在加载字体样式表和字体文件时阻止关键渲染路径对于普通用户来说,网站最重要的部分是什么?当然是内容。所以我们必须在页面加载期间尽快将内容呈现给用户。为此,必须将关键渲染路径精简为只有关键资源(如HTML和关键CSS),其余部分将在页面渲染后加载,包括字体。如果用户在糟糕的网络环境中浏览未优化的页面,坐在空白屏幕前等待字体文件和其他关键资源加载可能会非常烦人。除非用户非常有耐心,否则他可能会认为页面根本没有开始加载,放弃等待,关闭窗口。但是,如果延迟非关键资源的呈现并尽可能快地呈现内容,用户将能够浏览页面并忽略任何缺失的呈现样式(例如字体)——当然,如果字体不是内容的一部分。优化的网站会尽快呈现包含关键CSS的内容,并延迟非关键资源的加载。在第二条时间线的0.5s和1.0s之间出现字体切换,指示显示样式何时开始渲染。加载字体的最佳实践重新发明轮子是没有意义的。HarryRoberts已经很好地介绍了一种加载字体的最佳实践。使用来自GoogleFonts的深入研究和数据,他将其归结为四个步骤:将网站预链接到字体文件。异步预加载低优先级的字体样式表。在内容渲染完成后,使用JavaScript异步加载字体样式表和字体文件。为关闭JavaScript的用户提供替代字体。让我们使用Harry的方法来实现我们的字体:
注意字体样式链接“print”上的media=.浏览器会自动为打印样式赋予低优先级,并将它们从关键渲染路径中排除。加载打印样式表后,将触发onload事件,媒体将切换为默认的all值,字体将应用于所有媒体类型(屏幕、打印和语音)。Lighthouse对这种方法很满意!重要的是要注意自托管字体也可以帮助解决渲染阻塞问题,但这并不适用于所有情况。例如,使用CDN可能是不可避免的。在某些情况下,让CDN承担服务静态资源的重任可能是有益的。即使我们现在以最佳的非渲染阻塞方式加载字体样式表和字体文件,我们也引入了一个小的UX问题……无样式文本闪烁(FOUT)这就是我们所说的FOUT:为什么会发生呢?为了移除渲染阻塞资源,我们必须在页面内容被渲染(即显示在屏幕上)之后加载它。对于在关键资源之后异步加载的低优先级字体样式表,用户可能会看到从备用字体到下载字体的瞬间切换。不仅如此,页面的布局可能会发生变化,导致某些元素在网络字体加载完成之前看起来支离破碎。处理FOUT的最佳方法是使后备字体和网络字体之间的切换顺畅。为此我们要做的是:选择尽可能与异步加载的字体相似的系统字体。调整后备字体的样式(font-size、line-height、letter-spacing等),使异步加载的字体尽可能匹配后备字体特征。在异步加载的字体文件呈现后,立即清除回退字体的样式,并应用为新加载的字体准备的样式。我们可以使用字体匹配器为我们选择和计划使用的任何网络字体找到最佳替代字体和配置。在我们为备用字体和网络字体准备好样式后,我们可以继续下一步。在此示例中,Merriweather是要使用的字体,而Georgia是后备字体。一旦应用了Merriweather的样式样式,应尽量减少布局偏移,并且字体切换不应引人注意。我们可以使用CSS字体加载API来检测何时加载网络字体。为什么?Typekit的网络字体加载器曾经是比较流行的方式之一,虽然继续使用它或其他类似的库似乎很诱人,但我们需要考虑以下几点:它已经四年没有更新了,所以这意味着,如果插件有任何问题或需要引入新功能,很可能没有人会更新和维护它。我们已经使用HarryRoberts的代码来有效地处理异步加载,而不依赖于JavaScript来加载字体。如果您问我,使用像Typekit这样的库会为这样一个简单的任务带来太多的JavaScript代码。我想避免任何第3方库和依赖项,所以让我们自己找到解决方案,并在不过度设计的情况下使其尽可能简洁明了。尽管CSSFontLoadingAPI被认为是一项实验性功能,但它已经被大约95%的浏览器支持。但是,我们仍然必须提供后备字体,因为API将来可能会更改或被弃用。丢失字体的风险不值得冒这个风险。CSS字体加载API可用于动态和异步加载字体。我们决定不依赖JavaScript来处理诸如字体加载之类的简单事情,并使用带有预加载和预布线的纯HTML的最佳实践来解决它。我们将在API中使用一个函数来帮助我们检查字体是否已加载和可用。document.fonts.check("12px'Merriweather'");check()函数判断参数中指定的字体是否可用,返回true或false。字体大小参数值对于我们的用例并不重要,它可以设置为任何值。但是,我们需要确保:页面上至少有一个HTML元素包含至少一个字符并应用了网络字体声明。在我们的示例中,我们将使用。但是任何字符都可以使用,只要它对有视力的用户和无视力的用户都是隐藏的(不使用display:none;)。API跟踪已应用字体样式的DOM元素。如果页面上没有匹配的元素,则API无法判断字体是否已经加载。check()函数的参数中指定的字体正是CSS中调用的字体。在下面的演示中,我使用CSS字体加载API实现了字体加载侦听器。出于演示目的,加载字体及其侦听器将通过单击模拟页面加载的按钮来触发,以便您可以看到发生的变化。在常规项目中,这应该在网站加载和呈现后立即发生。那不是很好吗?感谢CSSFontLoadingAPI的良好支持,我们用不到30行JavaScript代码实现了一个简单的字体加载监听器。在此过程中,我们还处理了两种可能的极端情况:API出现问题,或者发生了一些错误,导致无法加载网络字体。用户在关闭JavaScript的情况下浏览网站。现在我们有了检测字体文件何时完成加载的方法,我们需要向回退添加样式以匹配网络字体,并了解如何更有效地处理FOUT。在替代字体和网络字体之间切换看起来很流畅,我们设法实现了不太明显的FOUT!在复杂的站点上,此更改将引入少量布局偏移,并且依赖于内容大小的元素看起来不会损坏或错位。幕后发生的事情让我们从HTML开始仔细研究前面示例中的代码。我们在元素中有代码片段,允许我们通过预加载、预链接和存在回退异步加载字体。
/*这里有个不间断的空格*/ 注意我们在元素上有一个硬编码的.no-js类名,它将在HTML文档完成加载时被删除。这会为禁用JavaScript的用户呈现网络字体的样式。其次,还记得CSS字体加载API如何要求至少一个字符HTML元素来跟踪字体并应用其样式吗?因为我们不能使用display:none;,我们添加了一个包含 字符的
以有效的方式将其隐藏在有视力和无视力的用户面前。此元素具有内联样式字体系列:'Merriweather'。这使我们能够在替代样式和加载的字体样式之间平滑切换,并确保正确跟踪所有字体文件,无论它们是否在页面上使用。请注意,该字符并未出现在代码块中,但确实出现了!CSS是简单的部分。我们可以利用HTML中硬编码的CSS类名或有条件地使用JavaScript来处理不同字体的加载状态。body:not(.wf-merriweather--loaded):not(.no-js){font-family:[fallback-system-font];/*Fallbackfontstyles*/}.wf-merriweather--loaded,.no-js{font-family:[web-font-name]";/*Webfontstyles*/}/*Accessiblehiding*/.hidden{position:absolute;overflow:hidden;clip:rect(0000);height:1px;width:1px;margin:-1px;padding:0;border:0;}JavaScript是魔法发生的地方。如前所述,我们使用CSSFontLoadingAPI的check()函数来检查字体是否加载成功。同样,字体大小参数可以是任何值(以像素为单位);它的font-family属性需要与我们加载的字体同名。varinterval=null;functionfontLoadListener(){varhasLoaded=false;try{hasLoaded=document.fonts.check('12px"[web-font-name]"')}catch(error){console.info("CSSfontloadingAPIerror",error);fontLoadedSuccess();return;}if(hasLoaded){fontLoadedSuccess();}}functionfontLoadedSuccess(){if(interval){clearInterval(interval);}/*Applyclassnames*/}interval=setInterval(fontLoadListener,500);这段代码是我们使用fontLoadListener()来设置一个定期运行的监听器的地方。此功能应尽可能简单,以便在计时器间隔内高效运行。我们使用一个try-catch代码块来处理任何错误并捕获任何问题,使得网页字体样式在JavaScript错误的情况下仍然可以使用,这样用户就不会遇到任何界面显示问题。接下来,我们使用fontLoadedSuccess()来监视何时加载字体。我们需要确保立即清除计时器,以避免之后进行不必要的字体加载检查。为了应用网页字体的样式,我们可以在这里添加一些需要的类名。最后,我们初始化定时器的周期。在此示例中,我们将其设置为500毫秒,因此该函数每秒运行两次。这是Gatsby的实现与一般的Web开发(甚至是常规的create-react-app堆栈)相比,Gatsby做事的方式不同,这使得实现此处介绍的内容有点棘手。为了使这更容易,我们将开发一个原生的Gatsby插件,因此在下面的示例中,所有与字体加载器相关的代码都位于plugins/gatsby-font-loader中。我们的字体加载器代码和配置将分为三个主要的Gatsby文件:插件配置(gatsby-config.js):我们将在项目中引入本地插件,列出所有本地和外部字体及其属性(包括字体名称和CSS文件URL),并拉入所有预连接的URL。服务器端代码(gatsby-ssr.js):我们将使用GatsbyAPI中的setHeadComponents函数根据配置在HTML中生成并包含预加载和预连接标签。然后,我们使用setPostBodyComponents生成隐藏字体的HTML代码,并将其包含在HTML文档中。客户端代码(gatsby-browser.js):由于此代码在页面加载和React启动后运行,因此它已经是异步的。这意味着我们可以使用react-helmet注入字体样式表链接。我们还将启动字体加载侦听器来处理FOUT。您可以在下面的CodeSandbox示例中检查Gatsby实现。我知道,有些事情很复杂。如果您只是想要简单的开箱即用解决方案来优化性能、异步加载字体和避免FOUT问题,我为此开发了gatsby-omni-font-loader插件。它使用本文中的代码,我正在积极维护它。如果您有任何建议、错误报告或代码贡献,请随时在GitHub上提交。结论内容可能是网站用户体验中最重要的部分。我们需要确保内容获得最高优先级并尽快加载。这意味着在加载期间尽量减少使用最少的表示样式(即内联关键CSS)。这就是为什么在大多数情况下网络字体被认为是非关键资源的原因——用户仍然可以在没有字体的情况下查看内容——所以在页面完成渲染后加载它们是完全没问题的。但这会导致FOUT和布局偏移,因此我们需要字体加载监听器来确保回退系统字体和网络字体之间的平滑切换。我想听听你的想法!在评论中让我知道您如何处理项目中的网络字体加载、渲染阻塞资源和FOUT问题。