Web Fonts 的优化:FOUT, FOIT 和 FOFT

发布于 大漠

在上一节中,我们一起探讨了 Web Fonts 和 系统字体之间的差异。在这一部分我们一起来探讨使用 Web Fonts 时,浏览器加载 Web Fonts 和渲染 Web Fonts 的策略。其实,聊 Web Fonts 就离不开 FOUTFOITFOFT 话题,特别是 FOUTFOIT 。简单地说,FOUT、FOIT 和 FOFT 都是浏览器渲染文本的三种不同的表现,特别是 Web Fonts 被引入到 Web 中时,浏览器对 FOUT 和 FOIT 的优化就没有停止过。

FOUT, FOIT 和 FOFT

聊 Web Fonts 就离不开 FOUTFOITFOFT 话题,特别是 FOUTFOIT 。简单地说,FOUT、FOIT 和 FOFT 都是浏览器渲染文本的三种不同的表现,特别是 Web Fonts 被引入到 Web 中时,浏览器对 FOUT 和 FOIT 的优化就没有停止过。

上面视频介绍了 FOUT 和 FOIT 的历史,来自于 《A historical look at FOUT and FOIT》一文

我们不去聊他们的历史以及整个演变过程,但我们要聊 Web Fonts 的优化,就需要对它们有一定的了解,因为我们后面有很多要做的事情都将围绕着 FOUTFOIT 展开。先从字面意思开始:

  • FOUT 是 Flash Of Unstyled Text 首字母缩写,中文意思是 无样式文本闪现
  • FOIT 是 Flash Of Invisibale Text 首字母缩写,中文意思是 不可见文本闪现
  • FOFT 是 Flash Of Faux Text 首字母缩写,中文意思是 伪文本闪现

Web Fonts 渲染表现

在 Web 中使用 Web Fonts时常会看到下图这样的两种现象: ​

简单地说,FOIT(不可见文本闪烁)和 FOUT(无样式文本闪烁)其实就是描述了浏览器处理页面加载字体加载 之间时间的两种主要方式。事实上,Web Font 的文件大小相对较大(特别是中文字体),而页面的其他部分很可能在字体下载完毕之前就已经下载完毕。因此,我们需要决定在等待字体时如何处理页面上的文字。我们基本上有两种选择:

  • 可以隐藏文本,直到字体准备好止(FOIT)
  • 可以先使用备用字体显示,然后字体加载完成将其与 Web Font交换(FOUT)

使用 Web Font 造成文本闪现的原因

使用 Web Font 造成文本闪现(FOIT 和 FOUT) 除了内在的原因之外(字体的差异),还有外在的原因(字体的加载和渲染)。先来看外在的原因,即 字体加载和渲染

通常,使用 CSS 的 @font-face 把 Web Fonts 嵌入到 Web 中。这些写在 CSS 文件中的样式规则,浏览器必须待文件下载结束并解析之后才能开始下载字体文件。而要真正地触发字体下载,还需要满足一些必备条件:

  • 合法的 @font-face 规则,src 给当前浏览器引入可支持的字体格式(现在主流浏览器一般使用.woff.woff2 两种字体格式)
  • 文档中有节点使用了 @font-face 中相同的 font-family (CSS 选择器与 DOM元素相匹配,且该选择器样式代码块中的 font-family 属性的值和 @font-face 声明的 font-family 值相同)
  • 在 Webkit 和 Blink 引擎中,使用该 font-family 的节点不能为空
  • 如果 @font-face 中指定了unicode-range ,出现的文字内容还必须落在设定的 Unicode 范围中

当上述所有条件满足,浏览器才会开始下载字体文件,这也意味着,浏览器不仅仅需要解析 CSS,还需要解析页面内容才能决定是否需要下载字体。然后再选择哪种方式处理页面文字上的渲染方式。

到目前为止,各浏览器对默认字体的渲染方式是不同的。如果你只是简单地在一个 @font-face 代码块中加载你的Web Font,然后用它来设计一些文字的样式,那么你基本上是让浏览器来决定如何处理页面加载和字体加载之间的这个间隙。 Chrome 和 Firefox 会将你的文本隐藏 3s,然后再回到系统字体上。然后,一旦准备好了,它们就会将其与 Web Font 交换。 IE 会在加载时立即显示一个备用字体(一般是系统字体),而不是隐藏文本,然后在准备好时将 Web Font换进来。最后,Safari 会在下载字体所需的时间内隐藏文本(字体下载失败,文本会永远隐藏)。所以,如果你不做这些额外的工作,你会得到 FOIT、FOUT 或 两者的组合,这一切都取决于你使用的浏览器。

如此一来,字体加载本质上归结为两个参数:“字体块超时(Font Block Timeout)” 和 “字体交换超时(Font Swap Timeout)”。真正的目标是在这两个参数之间取得平衡:

  • 字体块超时(Font Block Timeout):在显示备用字体之前,我们要隐藏文本多长时间?
  • 字体交换超时(Font Swap Timeout):在显示备用字体之后,我们要让浏览在多长时间内交换字体?

这也是谈论字体加载的高级方式,因为它更清楚地表明这两个概念可以在同一个页面加载中生效。如果我们对这两个参数进行分解,浏览器的默认行为看起来是这样的:

现在,一旦我们知道我们应该考虑哪些参数,就可以尝试决定哪些超时时间段给特定的项目带来最佳的用户体验。换句话说,我们可以决定如何处理字体加载前的时间,以及字体下载完成后的处理方法

众所周之,字体是有着自己独有的体系,这个体系是非常复杂的。也就是说,不是所有的字体都是一样的。这是一个问题,因为每个字体的基本指标(参数)都可能不同。每种字体的基线(Baseline)、X高度(x-height) 、中线(Median)和帽高(Cap height)都会略有不同。还有就是字母(文字)之间排列方式不同。由于字体之间的跟踪(Tracking)、前导(Leading)和字符间距(Kerning)会略有不同。

除此之外,还必须考虑到字体设计时人为的错误。与其他字体相比,一种字体中的字形在字体的边界框内的位置可能略有不同。强调这么多因素,我想表述的是,“当两种字体互换时,并不总是在每个字形的大小和位置方面得到一比一的替换”。可能发生的情况就是,内容可能向任何方向移动,即 造成布避偏移(Layout Shifts)

上图是 Telegraph 网站在 Moto G4 模拟器上的效果。文字字体从Georgia(它的备用字体)换成他们自己定义的 Web Font(Austin News):

@font-face {
    font-display: swap;
    font-family: Telesans Text Regular;
    src: url(ui/dist/static/fonts/Telesans-Text-Web-Regular.woff2) format("woff2"),url(ui/dist/static/fonts/Telesans-Text-Web-Regular.woff) format("woff")
}

@font-face {
    font-display: block;
    font-family: Austin News Headline Roman;
    src: url(ui/dist/static/fonts/Austin-News-Headline-Cond-Roman.woff2) format("woff2"),url(ui/dist/static/fonts/Austin-News-Headline-Cond-Roman.woff) format("woff")
}

@font-face {
    font-display: swap;
    font-family: Austin News;
    font-style: normal;
    font-weight: 300;
    src: url(ui/dist/static/fonts/austin-news-uprights-vf-basic-web.woff2) format("woff2");
    unicode-range: U+0020-007f,U+00a3,U+00e8,U+00e9,U+2013,U+2014,U+2018,U+2019,U+201c,U+201d,U+2022,U+2026,U+20ac
}

@font-face {
    font-display: swap;
    font-family: Austin News;
    font-style: normal;
    font-weight: 300;
    src: url(ui/dist/static/fonts/austin-news-uprights-vf-latin1-web.woff2) format("woff2");
    unicode-range: U+00a1-00a2,U+00a5-00e7,U+00ea-00ff
}

p {
    font-family: Austin News,georgia,times,serif;
}

正如你所看到的,由于字体度量(Metrics)指标不同,整个内容发生了变化。这个过程也就是我们所说的 FOUT(有时也称 FOUC,即 Flash Of Unstyled Content,无样式内容闪烁)。

这个问题在 CSS 字体模块 Level 4 (CSS Font Module Level 4)规范的“字体样式匹配”一节有详细的描述。通过阅读,浏览器经历了五至六套的字体匹配算法,试图将这个问题降到最低,其实浏览器也一直在解决这个问题,只至今还没有完全解决。还有,即使涉及到所有的自动匹配,在某些设备下,它仍然会发生在某些字体上。

到目前为止,FOUT 和 FOIT 是无法消除,我们能做的是平衡,让 Web Fonts 给布局偏移尽可能的少!

既然使用 Web Fonts 无法避开 FOUT 和 FOIT,那我们就只能去做选择。 要做选择,就要知道 FOUT 和 FOIT 具体是什么?两者差异是?

FOUT vs. FOIT

与其说 FOUT 和 FOIT 是浏览器渲染文本的一种现象,不如说它是字体加载的机制。如果你选择的是 FOUT,那么浏览器就会优先考虑尽快显示完整的内容,为此,在整个渲染过程中,FOUT 会先采用系统字体(也就是备用字体),直到找到并加载完相应的 Web Font。它专注于内容,尽管暂时忽略了布局。如果你的 Web 应该考虑内容进行优先排序时,那么 FOUT 是一个更好的选择(比FOIT更有优势),用户可以立即看到内容。

FOIT 与 FOUT 不同的是,FOIT机制决定了 页面渲染过程会有一个大约 3s 的空白档期,直到字体加载成功才会渲染。到目前为止,大多数浏览器在渲染文本之前会有一个 3s 的等待时间。如果自定义的 Web Font 加载成功,文本就会渲染,如果字体没有加载成功,文本会使用备用字体渲染(有个别浏览器不会采用备用字体渲染)。这在一个慢网络(比如,3G)环境下,用户有可能需要等上 3s 才能看到内容。

另外,FOIT机制致使文本渲染等待近 3s ,在这个等待过程中,页面在加载其他元素(比如图片)时也会出现延迟,甚至是所有布局渲染都会停止。

在 FOUT 和 FOIT 比较中,咱们必须考虑一些要点:

  • 表现:FOUT 大性能方面具有优势,因为它保证页面加载时间少于一秒,并且会使用系统字体(备用字体)显示内容;在FOIT 中显示文本要让用户等待 3s ,让用户等待近 3s 对于你的 Web 应用有可能是致命的伤害(用户可能没有耐心等你 3s),造在用户流失率
  • 外观:在外观方面,FOUT 会有一个字体交换过程(Web Font 字体文件加载前使用备用字体渲染,加载后用 Web Font渲染),页面会有明显的跳跃,对于用户来说,他有可能需要重新寻找位置。对于 FOIT 就不会有这个现象了,即使要等 3s ,FOIT 也不会产生字体变化,也就是说,一切都以预期的格式显示。如果仅从视觉上来说,FOIT 比 FOUT 更有吸引力
  • 用户体验:FOIT好的一面是“不会有字体的变化” 和 “内容在加载后即可用”,不好的一面是“等待 3s 成本太高,也是危险的,在这 3s 中内容不会有显示”;对于 FOUT 来说,用户等待文本渲染时间很短,平均只有一秒,用户能立即看到文本内容,不过其不好的一面是字体交互完成时会有明显的跳跃,视觉效果会有明显的变化,甚至致使用户需要重新寻找位置

FOFT:伪文本的闪烁

FOFT(Flash Of Faux Text)被称为伪文的闪烁。这种字体加载策略依赖于罗马字的变体(Roman Variant) ,而浏览器将“伪造” 斜体粗体 版本。在幕后,你加载斜体和粗体的变体,一旦有了“真实”的版本,就会渲染出来。采用这种技术,需要对一个字体进行子集(Subsetting),并对字体文件进行自我托管。它的优势是减少了字体渲染的阶段数量,并且大减少 FOUT(FOUC);其不足是会引入一个短暂的FOIT期。这种字体加载策略看上去还不错,但它需要相应的工具来辅助我们。

FOUT 和 FOIT 对页面性能的影响

我们花了很大的下个篇幅对 FOUT 和 FOIT 进行阐述。因为 FOUT 和 FOIT 是目前浏览器面对使用 Web Fonts 文本的渲染技术(策略)。虽然采用不同的字体加载策略(FOUT或FOIT)给用户带来不同的体验,但对于 Web 页面的性能方面来说,他们都致命的。不管是 FOUT还是FOIT或FOFT,都会影响 CWV的 FCP、LCP和CLS分数,也会触发页面的重排和重绘。下图展示了 FOUT和FOIT触发的重排(Reflows)和重绘(Repaints)的次数:

(上图来自:https://www.zachleat.com/foitfout/#8000,8000,9000,8000

为什么 Web Fonts 会导致布局偏移

我们把页面内容在没有用户交互(互动)的情况下发生移动这种现象称之为 意外的布局偏移(Layout Shifts),这种意外的布局偏移对用户体验是极其不利的。当浏览器加载 Web Fonts 时,容器元素(比如 divp)的大小发生变化时就导致了布局偏移,这主要是因为:**Web Fonts 和 系统字体高度、宽度或其他字体度量参数不同,造成容器内容文本宽度和高度不一样,从而改变容器的大小。**在页面布局时,浏览器将使用备用字体的尺寸和属性来决定容器元素的大小,即使你已经使用了 font-display: block 声明了一种 Web Font 来阻止系统字体。

简而言之,Web Fonts 和系统字体有着不同的度量参数,在两种字体交换(系统字体切换到 Web Fonts)时造成文本内容区域大小不同。用下面这个视频来描述,会有一个更清晰的认识:

检测布局偏移

有很多方法可以检测由于 Web Fonts 引起的布局偏移,最简单的方法是通过 WebPageTest 来检测你的页面并查看 CWV 中关于 CLS 的 filmstrip 视图,就可以清楚地看到你的页面在渲染时发彺了什么。比如华盛顿邮报(washingtonpost.com)有几处就是因为 Web Fonts 而导致的布局偏移。比如文章的标题文字在使用 Web Fonts(Postoni)的情况下和不使用 Web Fonts (备用字体garamond)情况下同一行单词数少一个(不同视窗宽度下略有差异),这导致布局偏移:

@font-face {
    font-display: fallback;
    font-family: Postoni;
    font-weight: 700;
    src: url(https://www.washingtonpost.com/wp-stat/assets/fonts/PostoniWide-Bold.woff2)
}

@font-face {
    font-display: fallback;
    font-family: Postoni;
    font-weight: 300;
    src: url(https://www.washingtonpost.com/wp-stat/assets/fonts/PostoniWide-Regular.woff2)
}

.font--headline, 
.font--magazine-headline {
    font-family: Postoni,garamond,serif;
    line-height: 1.1;
}

WebPageTest测试示例的CLS的 filmstrip 引起布局偏移的截图(下图黄色点划框框起的部分):

我们还可以通过 WebPageTestCloudflare Worker 看到用户的网格连接环境下,同时改变测试应用(Web 页面)的 font-display 属性值时页面的渲染结果。有关于这方面更详细的介绍,可以阅读 @Andy Davies 的《Exploring Site Speed Optimisatios With WebPageTest and Cloudflare Workers》一文。

你也可以使用浏览器开发者工具的性能测试选项来找到布局偏移,比如 Chrome 浏览器开发者工具性能选项(Performance),找到对应的布局偏移(Layout Shifts),寻找归属于文本元素的布局偏移:

你了可以使用 Font Style Matcher 这样的在线工具,先验证你将使用的 Web Fonts 和 备用字体在字体渲染方面的差异,也可以验证 FOUC,以及给布局偏移带来多大的影响: