编码与字体
字符概述
GB 与全角半角
大家都知道,程序中的所有信息都是以二进制的形式存储在计算机的底层,也就是说我们在代码中定义的一个 char 字符或者一个 int 整数都会被转换成二进制码储存起来,这个过程可以被称为编码,而将计算机底层的二进制码转换成屏幕上有意义的字符(如“hello world”),这个过程就称为解码。
在计算机中字符的编解码就涉及到 字符集(Character Set) 这个概念,他就相当于能够将一个字符与一个整数一一对应的一个映射表,常见的字符集有 ASCII、Unicode 等。
历史中的很长一段时间里,计算机仅仅应用在欧洲的一些发达国家,因此在他们的程序中只存在他们所理解的拉丁字母(如a、b、c、d等)和阿拉伯数字,他们在编码解码时也只需要考虑这一种情况,就是如何将这些字符转换成计算机所能理解的二进制数,此时 ASCII 字符集应运而生,他们在编码时只需要对照着 ASCII 字符集,每当在程序中遇到字符 a 时,就会相应的找到其中 a 对应的 ASCII 值 97 然后以二进制形式存起来即可。
ASCII
这种编码方式就被称为 ASCII 编码,从字符集对照表中可以看出,ASCII 字符集支持 128 种字符,仅使用 7 个 bit 位,也就是一个字节的后 7 位就可以将它们全部表示出来,而最前面的一位统一规定为 0 即可(如 0110 0001 表示 a)。
后来,为了能够表示更多的欧洲国家的常用字符如法语中带符号的字符 é
,又制定了 ASCII 额外扩展的版本 EASCII,这里就可以使用一个完整子节的 8 个 bit 位表示共 256 个字符,其中就又包括了一些衍生的拉丁字母。
ASCII 字符集沿用至今,但它最大的缺点在于只能表示基本的拉丁字母、阿拉伯数字和英式标点符号,因此只能表示现代美国英语(而且在处理英语当中的外来词如 naïve、café、élite 等等单词时,所有重音符号都不得不去掉)。而 EASCII 虽然解决了部份西欧语言的显示问题,但是当计算机传入亚洲之后,各国的语言依然不能完整地表示出来。
EASCII
在这个年代,每个国家就各自来对 ASCII 字符集做了拓展,最具代表性的就是国内的 GB 类的汉字编码模式,这种模式规定:ASCII 值小于 127 的字符的意义与原来 ASCII 集中的字符相同,但当两个 ASCII 值大于 127 的字符连在一起时,就表示一个简体中文的汉字,前面的一个字节(高字节)从 0xA1 拓展到 0xF7,后面一个字节(低字节)从 0xA1 到 0xFE,这样就可以组合出了大约 7000 多个简体汉字了。
为了在解码时操作的统一,GB 类编码表中还也加入了数学符号、罗马希腊的字母、日文的假名等,连在 ASCII 里本来就有的数字、标点、字母都统一重新表示为了两个字节长的编码,这就是我们常说的 “全角” 字符,而原来在 127 号以下的那些就叫 “半角” 字符了,这种编码规则就是后来的 GB2312。
这样,我们中国就有了属于自己的字符集了,但中国的汉字又是在是太多了,人们很快就发现 GB2312 字符集只能够那点汉字明显不够(如中国前总理 朱镕基 的 “镕” 字并不在 GB2312 字符集中),于是专家们又继续把 GB2312 没有用到的码位使用到其他没有被收录的汉字中。
后来还是不够用,于是干脆不再要求低字节一定是 127 号之后的内码,只要第一个字节是大于 127 就固定表示这是一个汉字的开始,不管后面跟的是不是扩展字符集里的内容。结果扩展之后的编码方案被称为 GBK 标准,GBK 包括了 GB2312 的所有内容,同时又增加了近 20000 个新的汉字(包括繁体字)和符号。
当时的各个国家都像中国这样制定出了一套自己的编码标准,之后当我们需要使用计算机与国际接轨时,问题出现了!国家与国家之间谁也不懂谁的编码,130 在法语编码中代表了 é
,在希伯来语编码中却代表了字母 Gimel
(ג
),在俄语编码中又会代表另一个符号。但是所有这些编码方式中,0–127 表示的符号依然都是一样的,因为他们都兼容 ASCII 码,这一点,如今也是一样。
Unicode
正如上一节中所说的,世界上各国都有不同的编码方式,同一个二进制数字可以被解码成不同的符号。因此,要想打开一个文本文件,就必须知道它的编码方式,否则用错误的编码方式解读,就会出现乱码。为了解决这个问题,最终的集大成者 Unicode 字符集出现了,它将世界上所有的符号都纳入其中,成功实现了每个数字代表唯一的至少在某种语言中使用的符号,目前,Unicode 字符集中已经收录超过 13 万个字符(第十万个字符在2005年获采纳)。值得关注的是,Unicode 依然兼容 ASCII,即 0~127 意义依然不变。
Unicode 表示的是一个字符集, 码点是 Unicode 字符集中唯一表示某个字符的标识。
码点的表示的形式为 U+[XX]XXXX,X 代表一个十六制数字,一般可以有 4-6 位,不足 4 位前补 0 补足 4 位,超过则按是几位就是几位,具体范围是 U+0000~U+10FFFF,大概是 111 万。按 Unicode 官方的说法,码点范围就这样了,以后也不扩充了,一百多万足够用了,目前也只定义了 11 万多个字符左右。
Unicode 标准只规定了代码点对应的字符,而没有规定代码点怎么存储,也就是说它本身只是个字符集而不是编码规则。这也是 UTF-8,UTF-16,UTF-32 的由来,它们是 Unicode 不同的实现。
图为联通两字在不同编码下的存储
由于历史的原因,文本文件出现的时候并没有像图片文件那样在文件内部约定其编码方式,只是后来新出现了其他的编码方式之后才开始有些约定。当文本程序打开一个文本文件时,如果没有在文件内部指明采用哪种编码方式,它就会采取“猜测”的方式去判断可能使用的编码方式,在“猜测”的时候就有可能出错了。前面查到“联通”2个字的GB2132编码为C1AA,CDA8,转换成二进制就是0b1100000110101010 ,0b1100110110101000,恰好能被误解为2字节的UTF-8编码:
图为联通两字在 UTF-8 与 GB2312 下的存储
可以看到,这两个字的 GB2312 表示刚好符合二字节的 UTF-8 的存储规则,提取其中有效bit位构成的Unicode编码为:0x006a,0x0368。按照 \u006a\u0368
输入后,会显示出 jͨ ,也就是乱码的情况了。
这种问题经常出现在 php 上,php 之锘,谁用谁知道。由于微软在自己的 UTF-8 格式的文本文件之前加上了EF BB BF 三个字节(在 UTF-16 中是 FF FE,即小端序,科普链接),但其他平台没有对 UTF-8 做 BOM 的解析,虽然在标准中这是可选的。
HTML Encode
&#x开头的是什么编码?
在 Node 中利用 cheerio 解析网页时,输出的中文内容都是以 &#x 开头的一堆像乱码一样的东西,尝试过各种编码都无效,而且神奇的是,将这一堆“乱码”保存成网页后,通过浏览器打开又可以正常显示。这到底是什么👻??
缩减后的示例代码如下:
1 | const cheerio = require('cheerio'); |
它叫做实体编码,在 HTML 中,某些字符是预留的,例如小于号「<」、大于号「>」等,浏览器会将它们视作标签。如果想要在HTML中显示这些预留字符,我们就要用到字符实体(character entities)。
在这里可以查到目前的 HTML 编码对照表(HTML Encode Character Reference)。
它会用 &
开始 ;
结束,例如 +
通过 HTML 转换后会变成 +
,它也可以用十六进制或十进制的方式表示:
1 | "+" = + = + = + |
我们比较熟悉的字符实体有空格「 」,小于号「<」,大于号「>」等。这样的格式比较语义化,容易记忆,但其实字符实体有其他的格式:
1 | &name; |
- 这三种转义方式都称作字符引用,第一种是字符实体引用,「&」符号后接预先定义好的 entity 名称。
- 后两种是字符值引用,数字取值为目标字符的 Unicode code point;以「&#」开头的后接十进制数字,「&#x」开头的后接十六进制数字。
从 HTML4 开始,numeric character reference 以 Unicode 为准,与文档编码无关。「你好」二字分别是 Unicode 字符 U+4F60 和 U+597D,十六进制表示的 code point 数值「4F60」和「597D」,同时也就是十进制的「20320」和「22909」。
所以在HTML中输入
1 | 你好 |
都会显示为“你好”。
在 JavaScript 中显示 Unicode
由于JavaScript只能处理UCS-2编码,造成所有字符在这门语言中都是2个字节,如果是4个字节的字符,会当作两个双字节的字符处理。
其中根据不同的场合有几种不同的表示方式,包括:
- 只使用到 Unicode 基本平面时 (
\u<码点>
)- 适用范围:
U+0000
到U+FFFF
。
- 适用范围:
- 有使用到 Unicode 辅助平面时(
\u{ <码点> }
)- 适用范围:所有 Unicode,特別是有使用超过
U+FFFF
的辅助平面文字符号 - ES6 引进,ES5 仅支持 U+FFFF 以内的 Unicode,不支持一个编码对应一个在 U+FFFF 范围之外的字符,原因是 ES5 支持的是 UTF-16,而 U+D800 - U+DFFF 专门保留以用于 UTF-16。由于它们不在 U+10000 - U+10FFFF 的范围内,UTF-16 不会使用代理对对它们进行单独编码,这些单独的代码点保留用作 UTF-16 中成对代码单元的前导值和尾随值。出现代理区的原因也很简单,和 ascii 一样,原本以为 65535 足矣容纳所有语言,之后发现小语种的语言,以及语言自身的进化(emoji),两个字节已不足以表示全部语言,但不应该把所有语言都在原基础上扩张一个或者是多个字符,因此出现代理区,告诉编码需要读取多少字节。
- 比如 💩 在 ES5 中需要被表示成
\uD83D\uDCA9
,在 ES6 中可以写成\u{1F4A9}
,许多场景的 emoji 都在这个辅助平面内,这也导致了另一个问题:'💩'.length === 2
,对这个问题有两种解决方式,一种是直接匹配/[\uD800-\uD8FF][\uDC00-\uDCFF]/g
的两个字符替换为一个,或者是通过 iterator 来切割:[...'💩'].length
- 适用范围:所有 Unicode,特別是有使用超过
- 只使用到 ASCII 字符时(
\x<码点>
)- 适用范围:
U+0000
到U+00FF
- 适用范围:
整理一下
1 | A = U+0041 = \x41 = \u0041 = \u{41} = \u{0041} |
总的来说,unicode 的确影响了很多操作字符串的方法, 包括没有提及的 substring
, slice
,使用时候可能需要注意。
其他乱七八糟的东西
b̳͕͖̑̓̒̈́ͤͥͣ͢͡ 这样一个字符,占位只显示一个位置,删除的时候得删半天,因为实际上长度为 13。以前在评论区经常能看到这些遮挡视线的字符,超出界限的原因是他们是组合附加符号(U+0300~U+036F),尽管在实际的表示法上、字符与附加符号是分开独立的,但该组合附加符号之字符在给定的上下文中将被视为单一的字位。
这是上面字符的 JS 表示:
1 | 'b\u0333\u0311\u0343\u0312\u0344\u0355\u0356\u0361\u0362\u0364\u0365\u0363' |
了解了这些,就可以拼凑出更多奇怪的文字表示,比如 Z͑ͫ̓ͪ̂ͫ̽͏̴̙̤̞͉͚̯̞̠͍A̴̵̜̰͔ͫ͗͢L̠ͨͧͩ͘G̴̻͈͍͔̹̑͗̎̅͛́Ǫ̵̹̻̝̳͂̌̌͘!͖̬̰̙̗̿̋ͥͥ̂ͣ̐́́͜͞。
真实世界中的其他 Bug 及该如何避免
看到了吧!Unicode 在 JavaScript 中奇怪的行为造成许多问题。许多开发者在处理字串时并没有考虑到补充平面的问题(🙋我就是其一),甚至包含知名的函数库 Underscore.string
的 reverse
也没有处理关于补充平面产生的问题。
毕竟处理这些问题的确很麻烦,而且好像没有必要?
在测试中参一坨💩吧 XD
一般处理单语种的开发者其实不太容易注意到这些问题。
无论正在写什么样功能的 JavaScript,试着在测试行为的字符串中加入 💩
然后看看会不会炸掉。 这有助于发现 Unicode 的问题。
在 CSS 中显示
1 | .content::before { |
这也是以前 iconfont 的实现基础。
URI Encode
在网络上最常被使用的是 UTF-8
的编码方式,它的特点是会用 %
开头,许多的网址或文字内容为了确保正确性,常常会先使用 URI Encode 后再显示。
在 JavaScript 中提供 encodeURI()
和 encodeURIComponent()
的方法可以将字符串转成 UTF-8。
相对应的解码方式则是 decodeURI()
和 decodeURIComponent()
。
字体概述
一个字体是共享一个共同的设计字符和符号的集合。
字体资源文件实际上是一个只包含数据的 DLL,没有代码。描述字体规格的标题和字形数据。由光栅渲染器知道它收集的是 ttf、otf 信息,这是不是很熟悉,字体文件格式?安装字体,就是把对应文字、表情信息存储到系统字体表中。字体表是一个内部数组,用于标识应用程序可用的所有非设备字体。
网络字体
通常浏览器只能使用系统自带的字体渲染文字。因为不同的系统安装的字体也不尽相同,所以相同的页面在不同的浏览器下显示效果也有不少差异。于是就诞生了 Web 字体。
Web 字体的基本思想是让浏览器从网络上自动下载需要的字体,然后再渲染文字。这不但可以统一网页在不同系统上的展示效果,还可以让设计师随心所欲地使用各种字体来表达情感。
但是,这么美好的东西在汉字文化圈却遇到了不小的麻烦。因为拼音文字一般只很少的字母(比如拉丁字母有二十四个),再加上部分标点符号和大字形式,它们的字体文体通常都不大。但常用汉字就有三四千,如果再加上几种主要的字体,那单个字体文体可能会达到几兆甚至几十兆字节之巨,很难应用到 Web 字体领域。也正因如此,汉字圈的 Web 字体发展比较迟缓。
网络字体的广泛采用发生在 2009 年至 2011 年左右,这要归功于主流浏览器增加了对两件事的支持:@font-face
CSS 声明和 WOFF 文件格式。
WOFF(及其后继者 WOFF2)是专门为网络字体创建的压缩文件格式。虽然常规 OpenType 字体(TTF 和 OTF 文件)可以用作 Web 字体,但不建议这样使用,因为它通常会违反许可协议——而且文件要大得多。WOFF 和 WOFF2 字体通常不能安装在台式计算机上。
请注意,网络字体是需要下载的资源,因此会影响页面加载时间的整体性能;也就是说,我们使用的网络字体越多,我们的网站加载所需的时间就越长。使用可变字体可能会抵消这一点。
自制字体
不同语言解析TTF或者OTF文件有不同方法,例如 JavaScript 可以使用 opentype.js,C语言 可以使用 otfcc,PHP的话可以使用 php-font-lib,Python 可以用 fontTools。这里使用 Python:
前置条件: Python 3.6 以上 安装
glyphhanger
和fonttools
举个例子,找到电脑上的微软雅黑(律师函警告),把需要的文本子集导入 test.txt,之后命令行执行 glyphhanger ./test.txt --subset=微软雅黑.ttf
,在当前目录下会生成一个 subset 的 ttf 字集,如果没有字体编辑软件,可以把字体导入到 https://font.qqe2.com/#,可以进行简单编辑,之后导出就可以使用了。
浏览器性能
在谷歌的页面体验排名算法中,有一个叫做布局偏移量的指标,即 the cumulative layout shift (CLS) score,是衡量页面不稳定程度的指标,是页面生命周期中所有意外布局变化的总和。
布局偏移的一些来源很容易解决:为动态元素预分配正确的空间,在图像上使用宽度和高度属性,并在 HTML 文档中优先显示可见元素。然而,布局偏移的一个常见原因却出奇地难以解决:无样式文本 (FOUT) 的闪烁。
为什么字体会导致布局偏移
当包含元素(例如一个
当用户第一次访问我们的网站时,如果网站使用的是 Web 字体,通常是需要下载所有的 Web 字体的。
在大多数情况下,当 HTML 文件已经在浏览器中加载完成后,Web 字体文件可能仍然还在下载中。
这种情况下,浏览器还无法使用 Web 字体。那我们应该怎么办呢?
通常有两种选择:
- 等到 Web 字体下载完成后再显示文本。
- 使用后备字体渲染文本,即用户设备本地上安装的字体。
但是这两种方法都会带来新的问题。第一种方法会出现隐形文本闪烁(FOIT — Flash Of Invisible Text)的问题。 而第二种方法则会出现无样式文本闪烁(FOUT — Flash of Unstyled Text)。
以上问题可能在你看来并没有怎么遇到过。这有可能是:
- 字体已经安装在你的设备上了
- 你的网络环境速度已经很快了
- 通常只有在第一次访问页面时才会存在这些问题,因为浏览器已经缓存字体文件了,所以再次访问就不会出现此问题了
只要用户首次访问我们的页面时需要下载Web字体,这个问题就会存在。然而,幸运的是,我们可以用一些现代的 CSS 来改进一些东西。
改进
要控制字体如何显示可以利用font-display
它包含在 @font-face
中:
1 | @font-face { |
它有几个值:
- block- 在等待网络字体时隐藏文本最多三秒钟,并在加载时始终交换网络字体
- swap- 尽快显示文本,并在加载时始终交换网络字体
- fallback- 隐藏文本长达 100 毫秒,然后仅在三秒内加载时才换入网络字体
- optional- 隐藏文本长达 100 毫秒,然后仅在可用时使用网络字体 - 从不交换
optional 是唯一保证没有布局偏移的字体显示值
图标字体
像 Font Awesome 和 Bootsrap 的 glyphicons 这样的图标字体集已经普及了网络字体在图标设计中的使用。不幸的是,这意味着你的图标在下载(通常)大字体文件之前不会呈现,并且有时会在字体文件未能及时下载时导致难看的⃞ 符号而不是真正的图标。图标字体对于性能和可访问性来说是不好的做法,应该尽快用 SVG 替换它们。
使用系统字体
Web 字体很受欢迎,因为它们允许设计人员在浏览器中保持一致的外观和感觉。在没有必要的情况下,系统字体将是呈现文本的最快方法。如果您当前的网络字体接近系统字体,您可以使用 Monica 的字体样式匹配器来调整字体设置,直到获得近乎完美的匹配。如果开发偷偷用经过调整的系统字体替换了他们的网络字体堆栈——设计师不一定能注意到其中的区别。
使用系统字体意味着文本将尽早呈现。我们现在还有使字体与操作系统匹配的方法,这可能比以前的后备选项(如 Arial 和 Helvetica)更具吸引力。为此,你需要按特定顺序列出所有操作系统的系统字体:
1 | body { |
加载更少的字体文件
下载一个或两个字体文件来呈现文本不会对速度产生巨大影响,下载五个或十个字体文件会!确保提供最小可行数量的字体文件是确保浏览器在布局时提供它们的最佳方式,从而减少FOUT布局偏移的可能性。让我们看看在保持设计的同时加载更少字体文件的一些技巧。
字体堆栈通常包括一堆不同的文件,从超轻斜体到超粗体。将 9 种字体粗细与正常和斜体变体组合在一起会产生 18 个独立的字体文件。
浏览器可以自己制作粗体和斜体版本的字体,这称为仿粗体和仿斜体。这可能意味着只需要一个常规粗细字体文件,但是应该由所有相关设计师进行测试,因为可能与预计的样式不同。
优化字体文件
如果我们必须下载一个字体文件,我们应该使它尽可能小以确保它可以快速下载。优化网络字体有两种关键方法:子集和格式。
子集字体
许多字体都有来自多个字母表的字形(字形是单个字符,如a
或&
)。如果你的网站仅以拉丁字母 (a - Z) 提供并且不使用连字(如é
),那么这些字形代表字体文件中的浪费字节。Roboto 是最流行的 Google 字体,有 277 个字形。
流行的 Roboto 字体中的字形
从此字体中删除非拉丁字符会导致woff2
文件大小缩小到六分之一!
要创建子集,最好从ttf
文件开始,找出需要保留的字形。Filament Group 创建了 glyphhanger 来完成其中繁琐的替换修改,阅读他们的博客文章,可以了解如何为页面创建独特的子集字体。要注意的是,缺少的字形将以备用的系统字体呈现。
有一组不同的格式可用于提供网络字体:ttf
、otf
、eot
、woff
和woff2
。ttf
和 otf
是原始字体文件,可能不应该发送到浏览器。 eot
使用LZ压缩进行压缩, woff
使用gzip压缩, woff2
使用brotli压缩。
字体库通常会提供许多或所有这些格式,但你实际上只需要woff2
。woff2
比woff
缩小大约 30%,并且所有现代浏览器都支持,仅支持woff2
将使应用的其他优化(例如子集化)变得更加简单。
实际上的精简并没有这么简单,因为一个字体文件由许多
表(table)
构成,这些表之间是存在关联的,例如maxp
表记录了字形数量,loca
表中存储了字形位置的偏移量。同时字体文件以offset table(偏移表)
开头,offset table
记录了字体所有表的信息,因此如果我们更改了glyf
表,就要同时去更新其他表。
unicode-range
另外,有一个 css 属性也可以给予子集字体一些支持。
简单来说我们可以将一个大的字体文件拆分成多个小文件,每个文件对应一组 @font-face 再用 unicode-range 指定文件的字符范围。比如下面的一段字体声明:
1 | @font-face { |
浏览器在渲染文字时如果碰到了1f1e9-1f1f5
范围内的 Unicode 字体才会下载对应的文件。这样就在一定程度上解决了单个中文字体体积过大的问题。
预加载
Web 浏览器不会下载不必要的字体,它们会等到渲染树构建完成后才知道需要哪些字体。这意味着只有在浏览器下载并解析 HTML 和 CSS 时才会请求 Web 字体,就在呈现文本之前。请注意,内联 CSS 不需要网络请求 - 这意味着您的字体可以在页面加载的早期获取。
如果我们知道在页面上呈现文本肯定需要 Web 字体,我们可以向浏览器承诺需要尽快下载它。这是预加载提示:
1 | <link rel="preload" href="/my-font.woff2" crossorigin="anonymous" as="font" type="font/woff2"> |
当浏览器解析这行 HTML 时,它会立即发出对字体文件的高优先级请求。这样做意味着当浏览器呈现文本时,网络字体更有可能被使用。
预加载请求会蚕食其他早期请求的带宽,谨慎使用!
新的提案
如果font-display: optional
您的设计无法实现,我们需要尝试尽可能将后备字体布局与网络字体相匹配。输入字体显示修饰符 (f-mods),也称为字体描述符。
F-mods 在字体描述符规范的提议更新中,其中包括四个新描述符:
- ascent-override (%) - 覆盖分配给上行的大小
- descent-override (%) - 覆盖分配给下行的行高
- line-gap-override (%) - 覆盖线之间的间隙
- advance-override (#) - 为每个字符设置一个额外的 advance,以帮助匹配行宽并防止单词溢出
前三者都会影响一行的高度:line box height = ascent + descent + line gap。基线位置 = line box top + line gap / 2 + ascent。例如,如果我们有ascent-override: 80%; descent-override: 20%; line-gap-override: 0%
,那么每个行框的高度为 1em(假设使用的字体大小为 1em),并且基线位于行框顶部下方 0.8em 处。描述advance-override
符在每个字符前添加一个 font-size 的一小部分作为间隙,例如font-size:16px; advance-override: 0.1;
将在每个字符前添加一个 1.6px 的间隙。
实现 f-mods 是使用src: local()
,这允许我们覆盖后备字体的显示以匹配网络字体:
1 | @font-face { |
获得 TTF 后,转到 FontDrop 并上传文件。打开 “Data” 选项卡并滚动到datahhea
**–
**Horizontal Header Table
. 在那里可以找到四个键值:ascender
、descender
、line-gap
和advanceWidthMax
。
F-mods 只真正修改垂直间距和定位。这意味着字符和字母间距仍然需要处理,否则可能会在不同点处出现单词断行,从而导致元素高度发生变化,从而导致布局发生变化。但是letter-spacing
和word-spacing
属性在 @font-face
声明中不可用,因此它们必须在正文或元素上声明。这意味着我们可能需要使用 使用字体加载 API,以防止布局偏移。
结论
布局变化不利于用户体验并且难以解决。渲染没有布局变化的文本比想象中的要复杂得多,如果可以应用font-display: optional
到 Web 字体,则可以完全避免布局偏移,否则这是一种与浏览器竞争的情况 - 试图在浏览器开始呈现文本之前将您的字体放入浏览器。
通过优化字体文件可以使浏览器加速:
- 用
woff2
最小化文件大小 - 从自己的域提供字体
- 预加载关键字体
- 将字体子集化为所需的字符
- 限制使用的字重变化的数量
- 探索可变字体
- 试用系统字体
- 使用 f-mods 减少字体交换的影响
一如既往,与真实用户一起测试所有更改,只有在看到积极的变化时才保留它们。可以在站点上,将正文设置为font-display: optional
,但标题字体和正文字体的斜体变体可以为font-display: swap
。在大多数情况下(多行标题除外),改变标题字体和斜体变体不会导致布局偏移,因此我认为这是设计和性能之间的良好折中。
参考链接
https://fonts.google.com/knowledge/glossary/web_font
https://fonts.google.com/knowledge/glossary/variable_fonts
https://simonhearne.com/2021/layout-shifts-webfonts/
https://www.ruanyifeng.com/blog/2014/12/unicode.html
https://meandni.com/2020/05/12/3619/
https://www.ruanyifeng.com/blog/2022/06/endianness-analysis.html