浅析css性能优化font-display的使用:控制字体加载和替换
在编写网站的时候,或多或少都会用到一些网络上的字体,CSS3 中虽然加入了对 Web Fonts(网络字体)的支持,但是浏览器对它们的加载和默认处理方式会极大的影响网站的性能和用户体验。例如默认情况下,在 Web Fonts 加载时,使用该字体的地方会显示空白,直到字体下载完成之后才会显示,这时通过改变 CSS 中的 font-display 属性,就可以避免这个问题。
一、什么是 web fonts?
在介绍 font-display 之前,先了解一下什么是 Web Fonts。在以前使用 CSS 指定字体时只能使用用户电脑本地上现有的字体,而由于每个用户电脑上的字体可能都不一样,所以能用的基本上就是操作系统内置的一些字体,例如微软雅黑,宋体,苹果苹方,这些也叫做安全字体(Web Safe Fonts)。
为了使字体显示正常,我们一般会通过 font-family 属性同时指定多个字体,如果第一个字体没有在操作系统中找到,就会使用下一个后备字体( Fallback Font ),以此类推:
* {
font-family: "PingFang SC", "Microsoft Yahei", sans-serif;
}
后来,CSS 开始支持 @font-face 这个指令,可以加载自定义的字体文件,这个时候可以把字体随网站一起发布,用户在浏览网站的时候,会下载 @font-face 中指定的字体。例如下边的代码加载了 fonts 目录下的 Raleway 字体:
@font-face {
font-family: 'Raleway';
font-style: normal;
font-weight: 500;
src: url(/fonts/raleway.woff2) format('woff2');
}
src 属性用于指定字体的位置,其中 url() 函数也接受网络地址,来加载第三方提供的字体文件,这样也催生了像 Google Fonts 这样的云字体服务。不过,基本上只有英文字体才适合 Web Fonts,因为它只有 26 个英文字母外加数字,体积小,适合在网络上传输,而中文光常用的就有 3000 个字符,所以一般只使用操作系统自带的,不过现在也有字体服务会根据网站上所使用的文字去动态的生成字体文件。
二、浏览器是如何加载字体的
浏览器经常会出现一些出乎我们意料的问题,而字体的加载就是其中之一。大多数浏览器在自定义字体还未下载之前会先隐藏文本。这就是大家所说的[FOIT(Flash of Invisible Text)](https://css-tricks.com/fout-foit-foft/)。
虽然现在在一般加载的过程中你可能不会看到这种情况,不过我们可以通过限制连接速度来观测浏览器的默认行为。可以看出,大部分浏览器会隐藏文本长达3s直到字体加载完成。其他浏览器,比如Safari会等待更长的时间。有些甚至永远都不会显示文本。
目前我们解决这类问题的方法是使用JavaScript-based脚本(比如 Font Face Observer 具体如何使用,可以看 github)来跟踪字体是否被下载。在字体被下载完成前,我们使用浏览器自带的字体来显示文本,直到我们通过JavaScript探测到字体下载完成,我们给我们的文档添加一个CSS类来应用我们的自定义字体。
比如,假设你有一个段落需要使用Open Sans Regular,那你就需要按以下的方法来实现:
p { font-family: "Arial", "Helvetica", sans-serif; }
在字体下载的时候,会先使用Arial 或者 Helvetica(这取决于你的浏览器支持什么字体)来显示文本。当通过JavaScript探测到Open Sans Regular字体下载完成,我们就将fonts-loaded添加到 < html >标签上,然后通过编写CSS来使用 Open Sans Regular 渲染该段落
.fonts-loaded p { font-family: "Open Sans Regular"; }
这个方法虽然能奏效,但是显得相当笨重。这时就有了font-display的用武之地啦。
@font-face {
font-family: "Open Sans Regular";
font-weight: 400;
font-style: normal;
src: url("fonts/OpenSans-Regular-BasicLatin.woff2") format("woff2");
font-display: swap;
}
在这个例子里我们通过只使用WOFF2文件来缩写字体。另外我们使用了swap作为font-display的值,页面的加载情况将如下图所示:
二、浏览器加载 Web Fonts 的时期
浏览器加载 Web Fonts 时按顺序会有三个时期:
1、阻塞期(Block Period)。在此期间如果字体没有加载完成,那么浏览器会使用 font-family 指定的字体列表中的后备字体(Fallback)进行渲染,但是显示为空白,也就是对于用户是不可见的。在此期间字体加载完成之后才能正常显示该字体
2、交换期(Swap Period)。跟阻塞期类似,但是在这个时期内,它会在字体加载时,先用后备字体渲染文本并显示出来(而不是显示空白),在此期间字体加载完成之后才能正常的显示该字体。
3、失败期(Failure Period)。如果字体加载失败,则使用后备字体显示文本。
至于每个时期有多长,是根据 font-display 属性的值来确定的。
三、font-display 介绍
font-display 确切的说不是 CSS 属性,而是专用于 @font-face 指令的描述符,它可以取如下几个值:
1、auto:这个是 font-display 的默认值,字体的加载过程由浏览器自行决定,不过基本上和取值为 block 时的处理方式一致。
2、block:在字体加载前,会使用备用字体渲染,但是显示为空白,使得它一直处于阻塞期,当字体加载完成之后,进入交换期,用下载下来的字体进行文本渲染。不过有些浏览器并不会无限的处于阻塞期,会有超时限制,一般在 3 秒后,如果阻塞期仍然没有加载完字体,那么直接就进入交换期,显示后备字体(而非空白),等字体下载完成之后直接替换。
3、swap:基本上没有阻塞期,直接进入交换期,使用后备字体渲染文本,等用到的字体加载完成之后替换掉后备字体。
4、fallback:阻塞期很短(大约100毫秒),也就是说会有大约 100 毫秒的显示空白的后备字体,然后交换期也有时限(大约 3 秒),在这段时间内如果字体加载成功了就会替换成该字体,如果没有加载成功那么后续会一直使用后备字体渲染文本。
5、optional:与 fallback 的阻塞期一致,但是没有交换期,如果在阻塞期的 100 毫秒内字体加载完成,那么会使用该字体,否则直接使用后备字体。这个就是说指定的网络字体是可有可无的,如果加载很快那么可以显示,加载稍微慢一点就不会显示了,适合网络情况不好的时候,例如移动网络。
那么在了解 font-display 之后,那么我们应该不难看出来,对于大部分情况应该把它的值设置为 swap ,这样在加载网络字体期间,使用后备字体进行渲染,加载完成之后在替换为指定的网络字体。
四、如果不支持font-display怎么办?
就只使用font-display属性。如果浏览器不支持这个属性,它只是没有办法使用该属性提供的便利之处,但是它不会破坏任何东西。
你可以既提供font-display属性也提供一个替代方案。如果时间和资源允许,推荐你选择这个方案。
如果你决定选择第二个选项,那么你首先需要去判断浏览器是否支持font-display属性。很幸运的是这是件很容易做到的事:
if ("fontDisplay" in document.body.style === false) {
/* JavaScript font loading logic goes here. */
}
通过判断之后的反馈,我们可以决定我们应该怎么做,是否应该使用第三方JavaScript脚本,比如Font Face Observer,或者是使用在Firefox, Chrome 和 Opera支持的字体加载API。
if ("fontDisplay" in document.body.style === false) {
if("fonts" in document) {
document.fonts.load("1em Open Sans Regular");
document.fonts.ready.then(function(fontFaceSet){
document.documentElement.className += " fonts-loaded";
})
}
}
在这里我们使用了字体加载API来为我们解决这个问题。一旦API知道字体加载完毕,就可以在< html >标签上添加fonts-loaded类,然后我们就可以通过编写CSS来逐步应用自定义字体。
p {
font-family: "Helvetica", "Arial", sans-serif;
}
.fonts-loaded p {
font-family: "Open Sans Regular";
}
很显然,我们肯定愿意使用一行CSS代码就能解决我们的需求,但是我们至少也需要有能力再需要的时候提供替代方案。随着时间的推移,我们可以预期这种解决方案,如字体加载API,在其他浏览器也将可以使用。
五、如果使用的是第三方字体供应商呢?
如果你使用的是第三方字体供应商,比如Google Fonts,你能做的事情是很有限的。font-display属性必须在@font-face指令内使用。因为你不能控制第三方字体供应商的CSS文件,所以你没有办法控制font-display属性更用说给他传递值了。
但是,现在使用谷歌的 Web Fonts 字体服务已经不需要我们用手动去写 @font-face 指令了,而是通过调用它的接口,直接返回一段 @font-face 指令 CSS 代码,同时它也支持 display=swap 参数,来让返回的 CSS 代码中,设置 font-display 为 swap ,这个可以从我的网站上看到
/* https://fonts.font.im/css?family=Raleway:500,700&display=swap */
@font-face {
font-family: 'Raleway';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(https://fonts.gstatic.font.im/s/raleway/v19/1Ptug8zYS_SKggPNyCAIT4ttDfCmxA.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
总之,font-display是一个很受欢迎的属性,除了增强网页样式,它还大大简化了JavaScript中冗余繁杂的任务。