menu

众所周知,字体是实现良好的设计、品牌推广、可读性和无障碍功能的基础。如今,所有还在使用的浏览器都已经支持 WebFont。WebFont 除了体现了字体的作用,还是实现用户体验和性能的关键所在。在 WebFont 推出以前,设计师的字体设计最终都是使用图片实现的。WebFont 的出现使设计师在可以达成所有上述目标的同时,还可以实现:文本可选取、可搜索、可缩放并支持高 DPI,无论屏幕尺寸和分辨率如何均可提供清晰锐利的文本渲染。

在我之前的一篇博客 「Web 性能优化(6)——WebFont 字体优化」 中,我简单介绍了一下针对 WebFont 在加载上的优化方案。现在随着姿势水平的提高,我现在打算来重新谈一下 WebFont。

浏览器如何渲染网页

划重点(TL;DR):当浏览器读取到 @font-face 时并不会立刻开始下载字体,而是在 DOM Tree 生成之后,浏览器发现 DOM Tree 中有非空的文本内容使用了 font-face 的字体才开始下载(IE9+ 会下载空节点)。

虽然很多人都已经把整个流程背的快要烂熟了,但是为了给文章凑字数我还是有必要再来一遍,嗯。

0000188.png

  • 浏览器请求 HTML 文档。
  • 浏览器开始解析 HTML 响应和构建 DOM。
  • 浏览器发现 CSS、JS 以及其他资源并分派请求。
  • 浏览器在收到所有 CSS 内容后构建 CSSOM,然后将其与 DOM 树合并以构建渲染树。
  • 在渲染树指示需要哪些字体变体在网页上渲染指定文本后,将分派字体请求。
  • 浏览器执行布局并将内容绘制到屏幕上。
  • 如果字体尚不可用,浏览器可能不会渲染任何文本像素。
  • 字体可用之后,浏览器将绘制文本像素。

如果当前字体不可用,浏览器就要去加载字体。在这个期间浏览器应该如何渲染文本,就有以下几种常见的方式:

FOUT (Flash of Unstyled Text)

当我们在 @font-face 中按优先级顺序定义了一系列字体(称之为 Font Stack)时,如果定义的最高优先级的字体在设备的字库中没有找到、或者引用了 WebFont 但是字体文件没有被加载,那么浏览器会继续轮询 Font Stack,直到找到可用的字体(这个过程就是寻找 fallback 字体)并先渲染出来;当自定义的字体文件被加载以后,浏览器会用这个字体文件重新渲染一遍画面。这有可能造成页面已经展示给用户以后页面的布局再次发生改动。而且,设计师并不喜欢 FOUT,因为这意味着有可能先让访客看到并不好看的备用字体、再看到好看的设计好的字体。但是 FOUT 不会因为字体文件无法加载而导致用户啥都看不到。IE 自从诞生之日起就在使用这种模式,现在 Edge 也在使用这种模式。

FOIT (Flash of Invisible Text)

这是浏览器处理在设备的字库中没有找到、或者字体文件尚未被下载时的另一种方案。如果检测到设定了当前优先级下有设置自定义字体文件,那么浏览器就会不显示任何内容,直到字体显示出来。这有可能造成访客可能需要等待很长一段时间才能看见网页的内容;如果网络环境较为恶劣,甚至有可能会导致有的内容永久不可见。Safari 曾经在很长的一段时间内使用这种模式,并且 iOS WebKit 仍然在使用这种模式,Opera 也曾短暂使用过。

FOIT 也有其优点,比如在显示 emoji 表情时,某些(大部分)emoji 表情在默认字体下会是一个方框,FOIT 避免了 fallback 字体时带来的不可预测的显示错误。

如果想要体验这两种方式的区别,你可以在 这里 尝试一下。
对于是使用 FOIT 还是 FOUT 的效果更好一直众说纷纭。不同的渲染策略各有充分的支持和反对理由。一些人觉得重新渲染令人反感,而其他人则愿意立即看到结果,并且不介意在字体下载完成后网页的自动重排。我们暂且不会参与这一争论,至少我个人的看法是,FOUT 给人的感觉更快(即时渲染比延迟更快)。但是如果你可以保证字体在超时之前加载(然而你并不能),FOIT 会感觉更稳定。

FOIT 3S

这应该是一个比较折中的解决方案,并且目前 Chrome、Firefox、Safari 都在使用。在 3s 内使用了自定义字体样式的使用 FOIT 模式,在一定时间内(1s,3-5s,也可能是 10s,具体看浏览器和版本)使用 FOIT,如果字体仍然没有加载出来就降级到 FOUT 以改善用户的浏览体验。

下面这个视频介绍了浏览器处理字体时方式演变的历史。

Magic Trick

Font Loader

Font Loader 目的提供了一种脚本编程接口来定义和操纵 CSS 字体,追踪其下载进度,以及替换其默认延迟下载行为。常见的方案有 Google Font Loader,fontfaceobserverWeb Font Loader。这本是应对 2015 年以前的浏览器(那是 FOIT 3s 还是个梦的年代)而生的。但是现在我们依然可以使用它,作为表明我们比浏览器聪明(我经常这么觉得)的方式。原理是通过 Event API 检查字体文件是否加载完成,通过操作 class 的方式决定什么时候用什么 font-family 或者干脆直接操作 display 自己重新实现一个 FOIT(开个历史的倒车),从而改善字体显示效果。使用 Magic Trick 的好处是至少兼容一些比较老的浏览器。

CSS Font Loading API

FOFT (Flash of Faux Text)

这是另外一个 Magic Trick。当你提供的自定义字体里没有粗体字重或者斜体时,浏览器会自己渲染一个“仿粗体”“仿斜体”(具体的相关细节可以阅读 Google Web Foundations 的 Web Font Optimization 的相关章节)。但是浏览器制作的“仿粗体”“仿斜体”显然比较的“粗劣”。所以 FOFT 就是在斜体和粗体的自定义字体文件加载之前先使用浏览器渲染的“仿粗体”“仿斜体”,直到自定义字体的粗体和斜体文件加载后再直接进入渲染代替“仿粗体”“仿斜体”。

Font Display

目前绝大部分现代的浏览器都已经支持通过 CSS 控制字体渲染(查看 Can I Use;查看 W3C 草案)。比如使用 font-display

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

要点在于,无论是 FOIT 还是 FOUT,虽然延迟加载减少了字节数,但也可能会延迟文本渲染。

通过内联优化字体渲染

有一个简单的替代策略可以代替 Font Loading API 或者 Font Display 来避免页面上什么都不显示,就是将字体文件内联到 CSS 样式表内。这是因为:

  • 浏览器会使用高优先级自动下载具有匹配媒体查询的 CSS 样式表,因为需要使用它们来构建 CSSOM。
  • 将字体数据内联到 CSS 样式表中会强制浏览器使用高优先级下载字体,而不等待渲染树。即它起到的是手动替换默认延迟加载行为的作用。

在你把字体 inline 到 css 里之前先回想一下,@font-face 使用延迟加载行为来避免下载多余的字体变体和子集。此外,通过主动式内联增加 CSS 的大小将对您的关键渲染路径产生不良影响。浏览器必须下载所有 CSS,然后才能构造 CSSOM,构建渲染树,以及将页面内容渲染到屏幕上。而且内联策略不那么灵活,不允许您为不同的内容定义自定义超时或渲染策略(因为你的字体和 css 绑定在一起并且变得一样重要),但不失为是一种适用于绝大部分浏览器并且简单而又可靠的解决方案。

详情可以参看我的「Web 性能优化(3)——探讨 data URI 的性能
另外,如果想要提高字体在加载时的权重,可以参考 Recource Hint 方案。我的博客也有专门讲述 Recource Hint 的文章:「Web 性能优化(5)——Preload 和 Server Push」。注意,在 preload webfont 时要注意设定 crossorigin 参数来避免 CORS 问题。

Google 在 Web Foundations 中是这么介绍的:

为获得最佳效果,请将内联字体分成独立的样式表,并为它们提供较长的 max-age。这样一来,在您更新 CSS 时,就不会强制访问者重新下载字体。

Smashing 杂志 写了他们优化字体的方法,他们使用 localStorage 来缓存字体。这样在第一次加载时成为 FOUT(或者 FOIT 3s),后续加载时成为(快速的)FOIT。

但是 Google 在 Web Foundations 中也强调:

您无需在 localStorage 中或通过其他机制存储字体,其中的每一种机制都有各自的性能缺陷。 浏览器的 HTTP 缓存与 Font Loading API 或 webfontloader 内容库相结合,实现了最佳并且最可靠的机制来向浏览器提供字体资源。

关于 localStorage 的详情可以参看「Web 性能优化(4)——localstorage 存储静态文件的意义」,这里我仅仅摘录我对 localStorage 的观点:

localStorage 作为一种持久性的存储容器,可以有效代替强缓存机制。浏览器缓存可能被清空,冷缓存可能会被热点文件挤出浏览器的缓存池,但是 localStorage 的存储毕竟是长久的,所以用来存储一些本身不需要经常更新的文件并不为过。
虽然有的人吐槽和质疑说手动实现一个操作 localStorage 的读写和更新机制不过是浪费时间,因为浏览器本身就已经有一整套 Cache Control 机制;但是你不得不承认,浏览器的 Cache 的操作就像一个黑盒,你不能控制它;如果你设置错了 cache control header,有可能会导致致命和后果。给 URI 添加 query 来控制缓存版本的方式不仅 dirty 而且是在浪费 CDN 和用户本地的存储空间。
虽然使用 Service Worker 操作 Cache Storage 解决了上述的问题,但是就目前来看,localStorage 的兼容性比 Service Worker 更好,而且缓存效果也更好。在已经有了相对成熟的 lsloader 和相关 API 来操作 localStorage 时,我倾向于在我的项目中使用 localStorage 作为缓存机制。

最高境界

说了这么多,真正“一劳永逸”来处理 WebFont 优化的方案听起来就像开历史的倒车:不在任何主要渲染路径使用任何外部字体。
比如说以阅读类为主的网站,文本比设计效果的美观更重要,这类网站就是主要渲染路径以大段文本内容为主。故这类网站如果要避免 FOIT FOUT,可以优先使用设备内置的字体显示文本,可以在不同的设备上获得更加的显示效果。

这个时候轮到推荐阅读的时间:

接下来是我摘录了一些常用的 font-family 写法,确保在不引用外源字体的情况下获得最好的浏览体验。

小米

font-family: "Helvetica Neue", Helvetica, Arial, "Microsoft Yahei", "Hiragino Sans GB", "Heiti SC", "WenQuanYi Micro Hei", sans-serif;

简书

font-family: -apple-system, SF UI Text, Arial, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif;

Ant.Design

font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", SimSun, sans-serif;

SegmentFault

font-family: -apple-system, "Helvetica Neue", Helvetica, Arial, "PingFang SC", "Hiragino Sans GB", "WenQuanYi Micro Hei", "Microsoft Yahei", sans-serif

Spectre.css

font-family: -apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",sans-serif

其它写法

font-family: "Helvetica Neue", Helvetica, Arial, "PingFang SC", "Hiragino Sans GB", "Heiti SC", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif;
/* 来自 huaji8.top */
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
/* 来自 imququ.com */
font-family: "-apple-system","Open Sans","HelveticaNeue-Light","Helvetica Neue Light","Helvetica Neue",Helvetica,Arial,sans-serif

小结和最佳实践

结合 Google Web Foundations 的「Web Font Otimization」总结一下使用 WebFont 的最佳实践。

  • 审核并监控您的字体使用:不使用外源 WebFont 就是对 WebFont 最佳的优化。不要在网页上使用过多字体,并且对于每一种字体,最大限度减少使用的变体(粗体、斜体)数量,必要时向浏览器提供的“仿粗体”“仿斜体”妥协,这有助于为您的用户带来更加一致且快速的体验。
  • 向每个浏览器提供优化过的字体格式:每一种字体都应以 WOFF2、WOFF 提供(如果需要兼容较旧的浏览器,你还需要 EOT 和 TTF 格式,同时需要应用 gzip 压缩,因为默认情况下不会对它们进行压缩;而 WOFF2 字体已经内建压缩,即使不进行 gzip 也可以减少传输数据量)
  • 指定重新验证和最佳缓存策略:字体是不经常更新的静态资源,确保提供尽可能长的 Cache Control 响应头,以实现减少重新载入字体消耗的流量和载入字体延迟导致较差的显示效果。必要时可以使用 localStorage 和 Service Worker 改善缓存的效果。
  • 使用 Font Loading API 来优化关键渲染路径:默认延迟加载行为可能导致 FOIT FOUT。您可以通过 Font Loading API 为特定字体替换这一行为,以及为网页上的不同内容指定自定义渲染和超时策略。对于不支持该 API 的较旧浏览器,您可以使用网页字体加载程序 JavaScript 内容库。
  • 在必要时提高字体加载的优先级:对于关键渲染路径的外源字体,为了避免 FOIT 和 FOUT,需要提升字体加载的优先级。Resource Hint 和 inline data URI 都是很好的备选项。同时高效和强制的缓存也能改善字体的显示效果。

顺便说一句,在接触和认识了一些涉及浏览器核心开发的 dalao 以后,我开始觉得前端的一些 Black Magic Trick 越来越有趣;我每隔一段时间都会重新阅读我曾经写过的 关于前端优化的文章;浏览器的行为是一种方式,然后我们欺骗它使用另一种方式,当它改变了这种方式时,我们欺骗它使用旧的方式。


本文作者:neoFelhz
本文链接:https://blog.nfz.moe/archives/webfont-123.html
本文采用 CC BY-NC-SA 3.0 Unported 协议进行许可,阅读 相关说明