前端字体优化

前端字体优化

最近在工作当中遇到了关于前端字体优化的问题,主要是公司有一款产品是一个在线的编辑工具,其中可以提供给用户各种不同的字体来进行编辑操作,所以在载入编辑区的过程当中会变得十分缓慢,所以特意抽了些时间研究了一下,在这里顺便记录一下

在正式展开之前,可以先了解一下 各平台的默认字体情况,下面我们就从如何加载自定义字体开始介绍

如何加载自定义字体

CSS3 当中,使用 @font-face 即可加载自定义字体了,使用 @font-face 可以定义某个特定字体资源的位置,其样式特征用于网页

1
2
3
4
5
6
7
8
9
10
@font-face {
font-family: 'SomeFont';
font-style: normal;
font-weight: 600;
src: local('Some Font Italic'),
url('/fonts/someFont.woff2') format('woff2'),
url('/fonts/someFont.woff') format('woff'),
url('/fonts/someFont.ttf') format('ttf'),
url('/fonts/someFont.eot') format('eot');
}
  • 使用 local() 指令,我们可以引用、加载和使用本地安装的字体
  • 使用 url() 指令,我们可以加载外部字体,并且该指令可以包含一个可选的 format() 提示,指示由提供的网址所引用的字体的格式
  • 对大型 unicode 字体进行子集内嵌以提高性能,比如使用 unicode-range 子集内嵌,并为较旧的浏览器提供手动子集内嵌回退
  • 减少风格字体变体的数量以改进网页和文本呈现性能

为了兼容不同的浏览器,我们一般会使用多个格式,也许你会遇到类似下面这样的写法

1
2
3
4
5
6
@font-face {
font-family: 'SomeFont';
src: url('someFont.eot'); /* IE9 Compat Modes */
src: url('someFont.eot?#iefix') format('embedded-opentype'), /* IE6 - IE8 */
url('someFont.woff') format('woff'), /* Modern Browsers */
}

可以注意到,上面例子当中有两个 src 属性,并且还有一个 ?#iefix 的后缀,它是有何作用的呢?绝大多数情况下,第一个 src 是可以去掉的,除非需要支持 IE9 下的兼容模式,这是因为在 IE9 中可以使用 IE7IE8 的模式渲染页面,微软修改了在兼容模式下的 CSS 解析器,导致使用 ? 的方案失效,由于 CSS 解释器是从下往上解析的,所以在上面添加一个不带问号的 src 属性便可以解决此问题

IE9 之前的版本没有按照标准解析字体声明,当 src 属性包含多个 url 时,它无法正确的解析而返回 404 错误,而其他浏览器会自动采用自己适用的 url,因此把仅 IE9 之前支持的 EOT 格式放在第一位,然后在 url 后加上 ?,这样 IE9之前的版本会把问号之后的内容当作 url 的参数,至于 #iefix 的作用,一是起到了注释的作用,二是可以将 url 参数变为锚点,减少发送给服务器的字符

字体格式

现在网络上使用的字体容器格式有四种 EOTTTFWOFFWOFF2,遗憾的是,无论选择的范围有多宽,都不会有在所有旧浏览器和新浏览器上都可以使用的单一通用格式

  • EOTIE 支持
  • TTF 具有 部分 IE 支持
  • WOFF 的支持最广泛,但它在许多较旧的浏览器中不可用
  • WOFF 2.0 支持 对于许多浏览器来说还未实现,

所以一般采用以下方式,采用多个样式,让浏览器自动采用自己所适用的

  • WOFF 2.0 变体提供给支持它的浏览器
  • WOFF 变体提供给大多数浏览器
  • TTF 变体提供给旧 Android4.4 版以下)浏览器
  • EOT 变体提供给旧 IEIE9 之下)浏览器
  • 还有一种 SVG 字体,因为兼容性和用途有限,可以忽略不提

但是在使用过程当中也会遇到问题,通常来说,字体文件一般加载都是非常缓慢的,因为中文字体文字数量庞大,字体文件也变得非常之大,页面加载之后,还需要很长的时间来下载字体,下载完成之后,才会正确显示

在用户看来,就是打开页面很久之后字体又变了,体验非常不好,所以为了解决这个问题,看了很多文档,也尝试了许多种方法,每种方法又可有优劣,所以在这里大致的总结一下,一般比较常见的有以下几种方案

压缩字体大小并且使用缓存

这个应该是比较常用的方法了,但是效果的提升不是很明显,一般的做法是在服务器开启 http/2,并对静态资源设置 E-TAGCache-Control 来进行缓存,也可以在服务器端配置 GZIP 压缩,可以有效的减小字体文件大小

还可以考虑使用 Zopfli 压缩处理 EOTTTFWOFF 格式,Zopfli 是一个 zlib 兼容压缩工具,该工具通过 gzip 大概可以减小越 5% 的文件大小,这种方法只是在一定层度上减小了字体文件的体积,却没用从根本上解决如果字体文件过大,加载缓慢的问题

font-spider

其实一开始也是准备使用 字蛛(font-spider) 这个库的,但是在目前这个项目当中,我们并不能知道用户添加的文本有哪些,输入的标题有哪些,所以也就不能使用这个方法了,但是还是抽空了解了一下使用方式

那么什么是 font-spider 呢?引用官方的话就是,字蛛是通过分析本地 CSSHTML 文件获取 WebFont 中没有使用的字符,并将这些字符数据从字体中删除以实现压缩,同时生成跨浏览器使用的格式,使用起来感觉很简单,首先进行安装

1
$ npm install font-spider -g

安装完成以后就可以在 CSS 中使用 WebFont

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 声明 WebFont */
@font-face {
font-family: 'myfont';
src: url('../font/myfont.eot');
src:url('../font/myfont.eot?#font-spider') format('embedded-opentype'),
url('../font/myfont.woff') format('woff'),
url('../font/myfont.ttf') format('truetype'),
url('../font/myfont.svg') format('svg');
font-weight: normal;
font-style: normal;
}

/* 使用选择器指定字体 */
.test > p {
font-family: 'myfont';
}

注意需要指定 ?#font-spider 后缀,关于这个属性,上面已经介绍过了

配置完成了以后,就可以在当前路径下,或者要压缩字体的 HTML 文件下执行以下操作

1
font-spider ./demo/*.html

这时,font-spider 就会帮助我们将页面依赖的字体将会自动压缩好,并且原 .ttf 字体是会备份的,下面是尝试过程当中遇到的一些坑

  • 格式相关问题,font-spider 主要依据 ttf 格式的文件来进行分析压缩的,所以 font-face 的路径必须存在 ttf 格式的,其他格式不行
  • 路径,引入路径要使用相对路径,否则会报 Web Font was not found
  • 局限性,font-spider 仅适用于固定文本,如果文字内容为动态可变的,新增的文字将无法显示为特殊字体
    • 解决办法是将备份还原,重新压缩
    • font-spider xxx.html 如果不添加 options,会默认备份原文件
  • 如果是 base64 形式的字体,可能会报不存在引入文件的错误,所以如果不想压缩某个字体包的话,就先注释其 font-face
  • 一旦压缩一次后,再次压缩别的是没用的
    • 比如说在一个文件夹里压缩了字体,生成了字体包,又在另一个文件夹里压缩字体,这两个文件夹的字体共用一个字体包和 font-face
    • 所以再次压缩的就是上一个压缩字体包进行压缩的,所以导致页面中有的字体没有转化过来
    • 解决办法,使用 font-spider file1/*.html file2/*.html file3/*.html 命令,压缩所有指定文件即可

fontmin

另外一个方法和 font-spider 差不多,但是感觉用起来更为方便一些,那就是 fontmin,简答来说,fontmin 的作用就是提取 ttf 字体文件中需要用到的文字,然后转换为 woff 文件输出,更为方便的就是我们可以手动指定输出内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const Fontmin = require('fontmin');

const fontmin = new Fontmin()
.src('assets/fonts/SourceHanSerifCN-Light.ttf')
.dest('build/fonts/')
.use(Fontmin.glyph({
text: '天地玄黄宇宙洪荒',
hinting: false
}))
.use(Fontmin.ttf2woff({
deflate: true
}))

fontmin.run((err) => {
if (err) {
throw err
}
})

下面是一些注意事项

  • src 制定了输入字体文件路径(必须是 ttf 文件)
  • dest 是输出路径
  • use(Fontmin.glyph({text, hinting})) 会生成一个只包含 text 字符的字体文件子集
  • hinting 指定所生成的 ttf 文件是否包含控制值表、字体程序区之类的信息(用于保留完整的 TrueType 轮廓描述信息)
  • use(Fontmin.ttf2woff({deflate: true})) 用来将上一步生成的 ttf 文件转化为 woff,进一步压缩大小

同样的,这个方法也只能用来处理固定的文本内容,由于在项目当中我们无法得知用户输入的具体内容,所以这个方案也被 pass 掉了

使用 Unicode-range 子集内嵌

在查找解决方法的过程中,发现了一个很少见的方法,即使用 Unicode-range 子集内嵌,原理是使用 unicode-range 描述符,我们可以指定一个范围值的逗号分隔列表,其中每个可以采用以下三种不同的形式之一

  • 单一代码点(例如 U+416)
  • 间隔范围(例如 U+400-4ff),指示范围的开始代码点和结束代码点
  • 通配符范围(例如 U+4??),字符指示任何十六进制数字
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@font-face {
font-family: 'SomeFont';
font-style: normal;
font-weight: 400;
src: local('SomeFont Font'),
url('/fonts/someFont-l.woff2') format('woff2'),
url('/fonts/someFont-l.woff') format('woff'),
url('/fonts/someFont-l.ttf') format('ttf'),
url('/fonts/someFont-l.eot') format('eot');
unicode-range: U+000-5FF; /* Latin glyphs */
}

@font-face {
font-family: 'SomeFont';
font-style: normal;
font-weight: 400;
src: local('SomeFont Font'),
url('/fonts/someFont-jp.woff2') format('woff2'),
url('/fonts/someFont-jp.woff') format('woff'),
url('/fonts/someFont-jp.ttf') format('ttf'),
url('/fonts/someFont-jp.eot') format('eot');
unicode-range: U+3000-9FFF, U+ff??; /* Japanese glyphs */
}

通过使用 unicode range 子集以及为字体的每种样式变体使用单独的文件,我们可以定义一个复合字体系列,访问者将仅下载变体及变体需要的子集,而不会强制他们下载他们可能从未在网页上看到或使用过的子集,在浏览器不支持 unicode range 的情况下,浏览器会下载所有字体

preload

最终敲定的方案是使用 <link rel=preload> 来提前加载字体文件,因为刚好在项目当中,用户在进入编辑区之前会有一个预览页面,所以在这个页面进行字体的预加载,然后利用浏览器的缓存功能,这样一来就可以保证用户在进入编辑区的时候不会经历漫长的等待,在此之前,我们先来看看网页加载中的字体加载过程,如下图所示

字体的延迟加载可能会延迟文本呈现,主要原因是由于浏览器必须构造呈现树,这依赖于 DOMCSSOM 树,在此之后,它将知道它将需要哪些字体资源来呈现文本,因此,会将字体请求很好地延迟到其他关键资源之后,并且在取回资源之前可能会阻止浏览器呈现文本,下面是一个简化版本的浏览器渲染过程

  • 浏览器请求 HTML 文档
  • 浏览器开始解析 HTML 响应并构造 DOM
  • 浏览器发现 CSSJavaScript 和其他资源并分派请求
  • 收到所有 CSS 内容之后,浏览器会立即构造 CSSOM,并将其与 DOM 树组合到一起来构造呈现树
    • 在呈现树指明需要哪些字体变体来呈现网页上的指定文本之后,会立即分派字体请求
  • 浏览器执行布局,并将内容绘制到屏幕上
    • 如果字体还不可用,浏览器可能不会呈现任何文本像素
    • 字体可用之后,浏览器会立即绘制文本像素

网页内容的首次绘制(在构建呈现树之后可以很快完成)和字体资源请求之间的先后顺序就会产生了 FOIT(Flash of Invisible Text),这种情况下浏览器可能会呈现网页布局而忽略任何文本,在不同浏览器之间实际的行为会有所不同

  • Safari 在字体下载完成之前会暂停文本呈现
  • ChromeFirefox 会暂停字体呈现最多 3 秒钟,3 秒钟之后它们会使用一种备用字体,并且字体下载完成之后,它们会立即使用下载的字体重新呈现一次文本
  • 如果请求字体还不可用,IE 会立即使用备用字体呈现,并在字体下载完成之后马上重新呈现

可以发现,这里有一个微观的过程就是字体显示时间线,简单的理解就是分为

1
字体阻止期 ==> 字体交换期 ==> 字体失败期

如果我们希望能够让浏览器更早一些的显示出正确字体的文字,那么就需要优化字体载入的时间

所以在这种情况下,我们就可以利用 <link rel=preload> 来提前加载字体文件,我们就可以把 <link rel=preload> 放到 HTML<head></head> 中,让浏览器提前下载字体文件,而不用等 DOM 树完成后等 CSS 下载后才去请求,由于下载字体使用了额外的 HTTP 请求,因此 http/2 可以大幅提高性能,它不需要受六个并发 HTTP 请求的限制

当然也可以采用动态加载的方式,也就是下面这样

1
2
3
4
5
6
7
8
9
function preLoad(href) {
const preloadLink = document.createElement('link')
preloadLink.href = href
preloadLink.rel = 'preload'
preloadLink.as = 'font'
preloadLink.type = 'font/woff'
preloadLink.crossorigin = 'anonymous'
document.head.appendChild(preloadLink)
}

当你在服务器上设置好 E-TAG(告诉客户端你的资源有没有变化)和 Cache-control(告诉客户端缓存时间长度)后,这些字体资源就不需要多次下载,以节省时间

使用 font-display 来控制字体阻止期

font-displayCSS 中新添加的属性,主要用来控制加载字体显示方式,有以下取值

  • auto
    • 默认值,典型的浏览器字体加载的行为会发生,也就是使用自定义字体的文本会先被隐藏,直到字体加载结束才会显示
  • swap
    • 后备文本立即显示直到自定义字体加载完成后再使用自定义字体渲染文本
    • 在大多数情况下,这就是我们所追求的效果
  • fallback
    • 这个可以理解为 autoswap 的一种折中方式
    • 需要使用自定义字体渲染的文本会在较短的时间(Google 浏览器大概在 100ms)不可见
    • 如果自定义字体还没有加载结束,那么就先加载无样式的文本,一旦自定义字体加载结束,那么文本就会被正确赋予样式
  • optional
    • 效果和 fallback 几乎一样,都是先在极短的时间内文本不可见,然后再加载无样式的文本
    • 不过 optional 选项可以让浏览器自由决定是否使用自定义字体,而这个决定很大程度上取决于浏览器的连接速度,如果速度很慢,那你的自定义字体可能就不会被使用

font-display 设置为 swap 后,意味着阻止期为 0,而交换期为无限长,这样一来你便可以避免网页出现空白文本,而立即用替换字体显示内容,一旦需要的字体下载完成后就可以替换原来的字体正确显示

如果你不知道选择那个作为 font-display 的属性值,可以选择 swap,它不仅提供了自定义字体和内容的可访问性之间的最佳平衡,它还提供了和使用 JavaScript 脚本相同的字体加载行为,如果你在页面上有想要加载的字体,但是最终也可以不加载,这时你就可以考虑使用 fallback 或者 optional 作为 font-display 的值

参考

# Essay

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×