关于@font-face加载前空白(FOIT)的解决方案

4,165 阅读4分钟

『 文章首发于GitHub Blog

问题

先来看一下FOIT的表现:

1615b236f18d9e87

简单来说FOIT(Flash of Invisible Text)就是文字使用了自定义的font-face,所以导致在自定义的字体加载完毕在之前,对应的文字会显示一片空白。 在老版本的浏览器中,会优先显示font-family中已经可以显示的候选字体,然后当font-face的字体加载完毕后,再变成font-face的字体,这被称作FOUT(Flash of Unstyled Text)。

下面是对FOUT和FOIT的一段英文解释,比较全面:

Remember FOUT? When using a custom font via @font-face, browsers used to display a fallback font in the font stack until the custom one loaded. This created a "Flash of Unstyled Text" — which was unsettling and could cause layout shifts. We worked on techniques for fighting it, for instance, making the text invisible until the font was ready.

\A number of years ago, browsers started to shift their handling of this. They started to detect if text was set in a custom font that hasn't loaded yet, and made it invisible until the font did load (or X seconds had passed). That's FOIT: "Flash of Invisible Text". Should a font asset fail or take a long time, those X seconds are too long for people rightfully concerned about render time performance. At worst, this behavior can lead to permanently invisible content.

目的

在实际的工程中,我们可能有几种对于font-face加载的需求:

  1. 恢复FOUT效果:在载入之前显示空白并不一定真的比先使用fallback字体体验更好,相比长时间的等待白屏,FOUT至少能让用户先看到内容。
  2. 在加载font-face字体完毕时能触发钩子函数:比如标题使用了font-face,标题一开始是隐藏的,直到font-face加载完毕再浮现,整个过程不希望发生FOUT,这就需要在font-face加载完毕时触发钩子函数来添加浮现效果的的CSS。

解决

第一个需求:使用font-display来实现FOUT

@font-face {
  font-family: Merriweather;
  src: url(/path/to/fonts/Merriweather.woff) format('woff');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

body {
  font-family: Merriweather,   /* Use this if it's immediately available, or swap to it if it gets downloaded within a few seconds */
               Georgia,        /* Use this if Merriweather isn't downloaded yet. */
               serif;          /* Bottom of the stack. System doesn't have Georgia. */
               
}

有几种不同的font-display,效果上会带来微妙的差别

  • font-display: swap:浏览器会直接使用font-family中最先匹配到的已经能够使用的字体,然后当font-family中更靠前的字体成功载入时,切换到更靠前的字体,相当于是FOUT。
  • font-display: fallback: 浏览器会先等待最靠前的字体加载,如果没加载到就不显示任何东西,这个过程大约持续100ms,然后按照顺序显示已经成功载入的字体。在此之后有大约3s的时间来提供切换到加载完毕的更靠前的字体。
  • font-display: optional: 浏览器会先等待最靠前的字体加载,如果没加载到就不显示任何东西,这个过程大约持续100ms,然后字体就不会再更改了(一般第一次打开某页面的时候都会使用fallabck字体,字体被下载但是没被使用,之后打开时会使用缓存中的字体)。

第二个需求:使用Web Font Loader

使用JS而不是CSS来引入字体,WFL会在字体引入的整个过程提供多个钩子函数,具体可以参考 官方文档Loading Web Fonts with the Web Font Loader

举个小例子,比如我想让某标题使用font-face字体。载入页面后,标题一开始是不可见的,直到自定义字体被成功加载后标题才向上浮动显示,当超过5s还没成功载入字体时将按fallback字体显示。这就需要判断自定义字体什么时候成功加载,什么时候载入失败。

function asyncCb(){
  WebFont.load({
    custom: {
      families: ['Oswald-Regular']
    },
    loading: function () {  //所有字体开始加载
      console.log('loading');
    },
    active: function () {  //所有字体已渲染
      fontLoaded();
    },
    inactive: function () { //字体预加载失败,无效字体或浏览器不支持加载
      console.log('inactive: timeout');
      fontLoaded();
    },
    timeout: 5000 // Set the timeout to two seconds
  });
}

Tips

还有一些关于font-face的知识我们也必须了解

  • font-face加载的时间:定义一个@font-face并不会立刻开始下载字体,而是在渲染树生成之后,浏览器发现渲染树中有非空的使用了font-face的字体才开始下载(IE9+会下载空节点)。
  • FOIT也有优点的时候,在显示emoji表情时,某些emoji表情在默认字体下会是一个方框,现代浏览器默认的FOIT避免了fallback字体带来的不可预测的显示错误。
  • Chrome, Opera有一个默认的3s的强制显示fallback字体的要求,如果在3s中无法正确载入font-face,会自动载入fallback字体。(但是Mobile Safari, Safari, Android Webkit没有,也就是在font-face加载失败时可能完全不显示文字)

参考