Web 字体优化 - 提升页面交互体验
Web 字体优化
本文是 网页字体度量及渲染 的下文。
本文将从字体加载、字体传输、字体渲染三个部分,介绍字体如何在对应生命周期内工作并给出优化方法。
先说结论:
字体优化其实就两种方案:
- 提高 web 字体加载 / 传输速度,在用户感知前加载完成
- 调整 web 字体或后备字体的渲染参数(Ascent / Descent /Line Gap),避免字体切换时出现布局偏移
Web 字体影响性能主要体现在两方面:
- 延迟文本渲染:在 web 字体完成加载前,浏览器会延迟文本渲染。这将影响 First Contentful Paint 首次内容绘制 (FCP)。有时也会影响 Largest Contentful Paint 最大内容绘制 (LCP)。
- 布局偏移:浏览器切换字体时有可能造成布局偏移,进而影响 Cumulative Layout Shift 累积布局偏移 (CLS)。
下载 web 字体时,当字体从后备字体切换为 web 字体时,会导致包含元素(例如)的大小发生变化,从而导致布局发生变化。当 web 字体的字体度量(Font Metrics)与后备字体相比不同时,就会出现这种情况。同时,布局页面时,浏览器将使用字体的尺寸和属性来确定包含元素的大小,即使你已声明。
注意:两种不同的字体是可能会导致布局发生变化的,但不是一定变化,这主要取决于字体的字体高度。
字体是网页典型的重要资源,没有字体可能导致页面白屏。因此,我们需要尽可能早的加载字体。
针对 CSS 内单独声明的,浏览器的字体加载可能有延迟。例如:
字体的延迟加载可能会延迟文本渲染。浏览器必须先构建依赖于 DOM 和 CSSOM 的 Render 树,然后才能知道它需要哪些字体资源来渲染文本。因此,字体会在其他关键资源请求之后延迟很长时间才开始请求,并且在获取字体资源之前浏览器可能阻塞文本渲染。
-
浏览器请求 HTML 文档。
-
浏览器开始解析 HTML 响应并构建 DOM。
-
浏览器发现 CSS、JS 和其他资源并调度请求。
-
浏览器在接收到所有 CSS 内容后构建 CSSOM,并将其与 DOM 树组合以构建 Render 树。
- 字体请求在 Render 树确定需要哪些字体来渲染页面上的指定文本后触发。
-
浏览器执行布局并将内容绘制到屏幕上。
- 如果字体尚不可用,浏览器可能不会渲染任何文本。
- 字体可用后,浏览器会渲染文本。
页面内容的第一次绘制与对字体资源的请求之间的“竞争”是造成“空白文本问题”的原因,浏览器可能会渲染页面布局但忽略任何文本。
注意,有一个误解就是只要在 内声明的 web 字体就一定会被浏览器下载。实际上,Render 树中只有真正用到的 web 字体才会下载,未用到的不会下载。下面代码示例中, 字体在 CSS 内的 和 中有声明,但 DOM 中并未使用到标签,因此字体不会被下载。
内联关键字体声明到 HTML head 标签中,而不是在单独的 CSS 文件中声明。这样可以让浏览器尽早发现字体声明而不是等到单独的 CSS 文件加载完成后才发现。
将放在 src 属性第一位,确保在本地已经安装了对应字体时不需要再进行网络请求。
如果知道页面肯定会用到的字体,那么可以利用资源优先级,使用 提前触发对 web 字体的请求,而无需等待创建 Render 树。通过预加载来防止布局偏移和不可见文本闪烁 (Flash of Invisible Text FOIT)
从 Chrome 83 开始,可以将 link rel="preload" 与 font-display: optional 组合来完全消除布局卡顿
属性会告诉浏览器将此资源作为字体下载,并帮助确定资源队列的优先级。
属性说明是否应使用 CORS 请求获取资源,因为字体可能来自不同的域。如果不设置此属性,浏览器将忽略预加载的字体。
字体预加载示例
字体无预加载示例
允许浏览器在一个 HTTP 请求正式发给服务器前预先执行一些操作,建立与服务器的连接,这包括 DNS 解析,TLS 协商,TCP 握手,这消除了往返延迟并为用户节省了时间。
- FontFace:提供 JS 接口来定义和操作 CSS 字体、跟踪它们的下载进度,并覆盖它们的默认延迟加载行为,可以理解成下载字体的 fetch 方法。例如,如果确定需要特定的字体变体,可以定义它并告诉浏览器立即启动字体资源的获取。
- FontFaceSet:消费 FontFace 下载的字体并查询字体下载状态。
属性 / 方法 | 用途 | 示例 |
---|---|---|
查询字体加载是否完成 | ||
查询当前已添加的 FontFace 数目 | ||
查询当前 FontFaceSet 内添加的FontFace 状态 | ||
添加 FontFace 到 FontFaceSet | ||
检查能否以特定字体渲染文本,可用来检测字体是否已加载 | ||
... | ... | ... |
更快的字体传输可以帮助字体更快渲染。如果字体传输足够快,那就可以避免布局偏移和 FOIT。
很明显,我们为了确保字体快速且正确地应用在我们网页上,我们必须让浏览器尽快下载我们的字体文件,在我们自己的 CDN 上托管字体将获得最佳性能。
WOFF 字体在 2012 年 12 月被 World Wide Web Consortium (W3C) 推荐使用,IE9+ 浏览器支持。WOFF 2 字体最早在在 2013 年 7 月 Chrome Canary 版本上可以使用,发展到现在,几乎已经成为自定义图标字体使用的标配,目前浏览器的兼容性已经相当不错了。
WOFF 2 标准在 WOFF 的基础上,进一步优化了体积压缩,带宽需求更少,同时可以在移动设备上快速解压。与 WOFF 中使用的 Flate 压缩相比,WOFF 2 是使用 Brotli 方法进行的压缩,压缩率更高,所以文件体积更小。
新的WOFF 2.0 Web 字体压缩格式平均要比WOFF 1.0小30%以上(某些情况可以达到50%+)
下面是一张 WOFF vs WOFF 2 字体大小对比图:
将字体作为 Base64 字符串嵌入到 CSS 中,从而无需额外的字体请求并确保在呈现文本时字体可用。但这个方法也不是绝对的好方法,它只适合一些小型字体文件,因为将字体文件转化为Base64字符串往往会增加体积。
定义每个资源支持的一组 Unicode 字符。这样就能将大型 Unicode 字体拆分成较小的子集(例如,中文、拉丁文和希腊文子集),并且仅需要在页面上下载呈现文本所需的字体集合。
Unicode 范围描述符可以指定通过逗号分隔的多个字符范围值,每个范围值都可以采用以下三种形式之一:
- 单个代码点(例如,)
- 区间范围(例如,):表示范围的开始和结束代码点
- 通配符范围(例如,): 字符表示任何十六进制数字
一句话解释,将页面真正用到的文字打包成字体集,而不是使用字体全集。
字蛛(font-spider)
字蛛是一个智能 WebFont 压缩工具,它能自动分析出页面使用的 WebFont 并进行按需压缩。
百度 fontmin
第一个纯 JavaScript 字体子集化方案。
可以先阅读 网页字体度量及渲染 了解字体度量,帮助理解本章节。
字体显示时间线
字体显示时间线基于一个计时器,该计时器在用户代理尝试使用给定下载字体的那一刻开始。时间线分为三个时间段,在这三个时间段中指定使用字体的元素的渲染行为。
- 字体阻塞周期
如果未加载字体,任何试图使用它的元素都必须渲染不可见的后备字体。如果在此期间字体已成功加载,则正常使用它。
- 字体交换周期
如果未加载字体,任何尝试使用它的元素都必须呈现后备字体。如果在此期间字体已成功加载,则正常使用它。
- 字体失败周期
如果未加载字体,用户代理将其视为导致正常字体回退的失败加载。
font-display 对应属性
以下表格中的阻塞时长是 W3C 提案中推荐值,各浏览器实现可能有差异。
属性 | 阻塞时长 | 交换时长 | 介绍 |
---|---|---|---|
auto | 浏览器默认 | 浏览器默认 | 字体显示策略由用户代理(浏览器各自默认行为) |
block | 3s | 无限 | 为字体提供一个短暂的阻塞周期和无限的交换周期。等待 web 字体时隐藏文本最多 3 秒, web 字体加载完成时交换。 |
swap | 100ms 或更少 | 无限 | 为字体提供一个非常小的阻塞周期和无限的交换周期。尽快显示文本, web 字体加载完成时交换。 |
fallback | 100ms 或更少 | 3 秒 | 为字体提供一个非常小的阻塞周期和短暂的交换周期。隐藏文本最多 100 毫秒, web 字体 3 秒内加载完成时交换,超过三秒保持展示后备字体。 |
optional | 文本初次渲染前的时长 | 无 | 不阻塞文本渲染,并且没有交换周期。 web 字体在文本首次渲染前加载完成则展示 web 字体,否则展示后备字体,从不交换。 |
注意:下图是很多博文的配图,和 W3C 的规范 及浏览器实现是有出入的!!!!!!
不同的 策略需要在页面性能和样式之间进行平衡。因此,很难给出推荐的方法,因为它确实取决于个人偏好、web 字体对页面和品牌的重要性,以及字体延迟切换带来突兀的用户体验。
对于绝大多数页面,基本都适用于以下三种方案:
- 性能优先:使用 optional 是唯一保证不发生布局偏移的字体显示值。 文本渲染几乎无延迟。并且确保不会发生因为字体切换造成的布局偏移。但缺点也很明显,如果文本首次渲染前 web 字体未加载完成,后备字体可能样式并不满足 UI 要求。
- 需要文本尽快展示且需要确保使用 web 字体:使用 swap 确保了文本展示几乎无延迟,但可能由于 web 字体加载慢导致布局偏移。所以需要尽量让字体尽快加载完成。
- 确保文本展示使用 web 字体:使用 block 让文本有 3s 的不可见时间,web 字体基本可以在 3s 内加载完成,这样文本展示的时候就会自动使用 web 字体。但仍然存在由于 web 字体加载慢导致布局偏移的问题,同时文本 3s 的不可见时间也是一个负面影响。
CSS 描述符为与此字体关联的字形轮廓和指标定义乘数。这使得在以相同字体大小呈现时更容易协调不同字体的设计。
如何使用:
如下示例,结合 网页字体度量及渲染 文章内容,可发现 是等比放大了字体度量的所有参数,Ascent / Descent / Line Gap 均被放大 150%。
这个方案也存在很明显的问题,不同字体之间切换的 百分比需要手动调试计算。浏览器目前的兼容性也很一般。
使用 、 、 属性来调整字体。这 3 个 CSS 属性作用都是类似的,都是在 自定义字体中设置文字的上、中或下间隙大小。
W3C 文档 中的介绍并不明确,具体计算方式见下表。Ascent / Descent / Line Gap / em 的取值见之前写的文章:网页字体度量及渲染
属性 | 描述 | 值 | 介绍 |
---|---|---|---|
ascent-override | 设置上悬线距离基线的距离 | normal | 默认值,由字体文件决定: (Ascent / em) * font-size。初始值为 Ascent / Em Size |
<percentage> | 范围从 0% - ∞,值越大,文字位置越低。具体值大小为:percentage * font-size | ||
descent-override | 设置下悬线距离基线的距离 | normal | 默认值,由字体文件决定:(Descent / em) * font-size。初始值为 Descent / Em Size |
<percentage> | 范围从 0% - ∞,值越大,文字位置越高。具体值大小为:percentage * font-size | ||
line-gap-override | 设置行间距> 行间距=行高 - 字体大小> 行高:line-height 为 normal 时的行高> 字体大小:每个字符的内容高度(不是font-size) | normal | 默认值,由字体文件决定:(Line Gap / em) * font-size。初始值为 Line Gap / Em Size |
<percentage> | 范围从 0% - ∞,值越大,行间隙越大。具体值大小为:percentage * font-size |
下面以 Catamaran 字体为例(为了展示 Line Gap,此处手动将 Catamaran 字体的 HHead Line Gap 值从 0 调整为 500)。由下图可得知 Catamaran 字体的各个参数(重点关注 110%、54%、50% 这三个值):
- Ascent: 1100
- Descent: 540
- Line Gap: 500
- Em Size: 1000
- ascent-override : normal=Ascent / Em Size= 110%
- descent-override : normal=Descent / Em Size= 54%
- line-gap-override : normal=Line Gap / Em Size= 50%
下面由几个示例来说明这三个 CSS 属性如何使用。示例中, 值为 normal ,红色文字为初始文字位置,蓝色文字为调整后文字位置,浅灰色为文本内容高度,深灰色为半行距(具体颜色说明见前文:网页字体度量及渲染)。
ascent-override
值越大文字行高越大。
示例 1:
由下图可见, 设为 0 时,文字的 Ascent 部分的高度完全没有了,行高塌陷了一大部分。但因为文字基线位置不变,所以相比于自己的内联行盒,文字整体朝上移动了。
示例 2:
由下图可见, 设为 110% 时,文字的 Ascent 部分恢复成了默认高度。
descent-override
值越大文字行高越大。
示例 1:
由下图可见, 设为 0 时,文字的 Descent 部分的高度完全没有了,行高塌陷了一大部分。但因为文字基线位置不变,所以相比于自己的内联行盒,文字整体朝下移动了。
示例 2:
由下图可见, 设为 54% 时,文字的 Descent 部分恢复成了默认高度。
line-gap-override
属性的作用是设置这个字体的行间距(行间距=行高 - 字体大小)。
设置要想生效,则对应字体所在的 属性值必须是 normal,无论是数值,长度值还是百分比值都会让 属性没有效果(只有当行高为 normal 时,最终的行高大小才由字体决定)。
值越大文字行高越大。
示例 1:
由下图可见, 设为 0 时,绿色文字的 Line Gap 部分的高度完全没有了,行高上下均塌陷了一部分。但因为文字基线位置不变,所以相比于自己的内联行盒,文字未移动。
示例 2:
由下图可见, 设为 50% 时,绿色文字的 Line Gap 部分恢复成了默认高度。
多字体实战
示例使用的两种字体对应的字体度量参数如下表。
属性 | Ascent | Descent | Line Gap | Em Size | Font Size |
---|---|---|---|---|---|
Catamaran | 1100 | 540 | 500 | 1000 | 100px |
MILanPro | 1044 | 282 | 0 | 1000 | 100px |
如下图为未使用任何 CSS 参数调整字体的示例,红色文字字体为 Catamaran,绿色文字字体为 MILanPro,绿色文字行高明显比红色文字小。如果 Catamaran 为 web 字体,MILanPro 为后备字体,那么在从 MILanPro 切换为 Catamaran 时,就会发生布局偏移。
将 MILanPro 字体 、、 分别调整为 Catamaran 字体的默认值,如下图为调整了CSS 参数后的渲染结果。虽然从字母 i (字母 i 上的点,红色为圆形,绿色为方形)可以明显看出两种字体渲染的文字样式的区别,但最终渲染文字的 Ascent / Descent / LineGap 尺寸完全一致。
注意,Ascent / Descent / LineGap 尺寸完全一致只是保证了两种字体的默认行高(line-height)完全一致,但不同字体相同字符的宽度不一定是一致的。下表为大写字母 X 在 Catamaran 和 MILanPro 字体中的宽度:
大写字母 X | 宽度 | FontForge 截图 |
---|---|---|
Catamaran | 588 | |
MILanPro | 691 |
如下图所示,红色为 Catamaran 字体,绿色为 MILanPro 字体,已经调整了 MILanPro 字体的度量参数,使两种字体的度量参数(Ascent / Descent / LineGap)完全一致。最终不同字体相同文本的宽度明显不一致。
字体优化其实就两种方案:
- 提高 web 字体加载 / 传输速度,在用户感知前加载完成
- 调整 web 字体或后备字体的渲染参数(Ascent / Descent /Line Gap),避免字体切换时出现布局偏移
- 快速加载:web.dev/fast/#
- MDN font-display: developer.mozilla.org/zh-CN/docs/…
- Preconnect: www.keycdn.com/support/pre…
- FontFace: developer.mozilla.org/en-US/docs/…
- FontFaceSet: developer.mozilla.org/en-US/docs/…
- How to avoid layout shifts caused by web fonts: simonhearne.com/2021/layout…
- Exploring x-height & the em square: fonts.google.com/knowledge/c…
- Deep dive CSS: font metrics, line-height and vertical-align: iamvdo.me/en/blog/css…
- 网页字体度量及渲染: juejin.cn/post/724214…