网站字体压缩

写在前面

在修改网站字体时,用到了第三方字体库,但是该字体库有20+MB大小,加载速度很慢。网速很慢的话,极其影响用户体验,还可能致使字体无法生效。换字体是不可能的,特此记录一下如何压缩字体,来加快访问速度。

据测试,字体从20.7MB压缩至1.65MB,压缩率为92.03%。

字体格式科普

常见的字体格式有以下几种,这里只作简单介绍。

TTF

TTF(TrueTypeFont)是Apple 公司和Microsoft公司共同推出的字体文件格式,随着 windows 的流行,已经变成最常用的一种字体文件表示方式。

这种格式的字体文件体积比较大,以思源宋体为例,字体文件可以达到 24MB+,通常只用作安装到计算机中的字体,或者在网页中设备不支持 WOFF2 字体情况的兜底处理。

OTF

OpenType,是一种可缩放字体(scalable font)电脑字体类型,采用PostScript格式,是微软公司与Adobe公司联合开发,用来替代TrueType字体的新字体。这类字体的文件扩展名有.otf.ttf.ttc,类型代码是OTTO,现行标准为OpenType 1.9。

可以理解为和 TTF 字体差不多,这里我们主要讨论体积问题,OTF 字体文件体积也很大,基本和 TTF 差不多。

WOFF & WOFF2

Web开放字体格式(Web Open Font Format,简称WOFF)是一种网页所采用的字体格式标准。此字体格式发展于2009年,由万维网联盟的Web字体工作小组标准化,现在已经是推荐标准。此字体格式不但能够有效利用压缩来减少文件大小,并且不包含加密也不受DRM(数字著作权管理)限制。

这是专门给网页使用的字体格式,体积非常小,实测压缩思源宋体字体文件,可以把体积压缩到 OTF 字体 70% 的大小。

WOFF 和 WOFF2 的区别在于:

WOFF本质上是包含了基于SFNT的字体(如TrueTypeOpenType或其他开放字体格式),且这些字体均经过WOFF的编码工具压缩,以便嵌入网页中。WOFF 1.0使用zlib压缩,文件大小一般比TTF小40%。而WOFF 2.0使用Brotli压缩,文件大小比上一版小30%。

因此,在网页中,一般推荐直接使用 WOFF2

压缩方法

这部分是正式的压缩方法了,主要分为两步,分别是:取子集、压缩。

前置条件:

  • 已安装Python环境
  • 已安装pip

安装压缩工具

这里我使用到的是 Python 的一个库:fonttools,使用最新版 Python 的 pip 命令安装即可在 Shell 中使用:

1
pip install fonttools

提取字体子集

中文汉字数量很多,以思源宋体为例,思源宋体遵循 GB 18030 和通用规范汉字表,包含 8105 个规范字(来源:少数派),可能还有其他语言的字符,实际字符数量肯定是远超这个数字的。

实际上,常用汉字数量也就 3500 个左右,如果你的文本相对固定,可以考虑删减掉其他不常用的汉字。

极端做法是只保留文本中出现的字符,其他的全部删掉,但是我个人更倾向于折中保留 3500 汉字,在未来如果修改了文本,也不至于每次都要重新压缩一遍字体。

这种删减字符的做法叫**“取子集”**。取子集我们需要定义一个纯文本文件,里面包含所有要保留的字符。

这里提供两种方法,一种是使用整合好的子集,另一种是自行制作子集。

三选一下载即可,Unicode字符集的整合较为全面,但是压缩后的字体文件体积会比文本字符集的大,优点是支持的字符更多。根据你的需求选择即可。

文本字符集:现代汉语常用 3500 字

Unicode字符集:汉语Unicode字符集(直接网页打开,另存为txt文件)

通用Unicode字符集:通用字符集(直接网页打开,另存为txt文件,支持英文&日文&中文简繁&常用符号,但压缩率不如上述两个字符集)

此方式生成的字符集更小,压缩率也更高,但是缺点是发布新文章后,如果文章中有上次没有的字符,则会导致字体丢失,这时候就需要重新生成子集并压缩。

Hexo网站为例,先执行hexo g指令生成你的网站public文件夹,然后通过脚本遍历此目录下的所有文件,提取出所有字符,并将其写入一个纯文本文件中。
这里提供一个用于提取字符的python脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import os
import re
from collections import defaultdict

# 要提取的文件类型
extensions = ('.txt', '.html', '.js', '.css', '.xml', '.json')

# 字符分类
categories = {
'Latin': set(),
'Chinese Characters': set(),
'Chinese Simplified & Traditional': set(),
'Symbols': set(),
}

# 字符的Unidcode范围
latin_range = range(0x0000, 0x0100)
chinese_range = range(0x4E00, 0x9FFF)
symbols_range = [
range(0x2000, 0x206F), # General Punctuation
range(0x3000, 0x303F), # CJK Symbols and Punctuation
range(0xFF00, 0xFFEF), # Halfwidth and Fullwidth Forms
]

# 分类字符
def categorize_character(char):
code_point = ord(char)
if code_point in latin_range:
categories['Latin'].add(f'{code_point:04X}')
elif code_point in chinese_range:
categories['Chinese Simplified & Traditional'].add(f'{code_point:04X}')
elif any(code_point in r for r in symbols_range):
categories['Symbols'].add(f'{code_point:04X}')
else:
categories['Chinese Characters'].add(f'{code_point:04X}')

# 遍历文件
for root, dirs, files in os.walk('./public'):
for file in files:
if file.endswith(extensions):
file_path = os.path.join(root, file)
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
for char in content:
categorize_character(char)

# 输出结果
output_str = ''
for category, chars in categories.items():
print(f"# {category}")
output_str += f"# {category}\n"
for char in sorted(chars):
# print(char)
output_str += f"{char}\n"

with open('unicode_set.txt', 'w+', encoding='utf-8') as f:
f.write(output_str)

将脚本文件放在博客根目录下,运行脚本即可生成unicode_set.txt文件。

压缩字体

使用以下命令即可对字体文件取子集:

1
2
3
4
# 对于文字子集
fonttools subset "$input_file" --text-file="$text_file"
# 对于Unicode子集
fonttools subset "$input_file" --unicodes-file="$text_file"

变量含义:

  • $input_file:输入的字体文件。
  • $text_file:定义保留字符的纯文本文件路径。

压缩命令执行完毕后,会在当前目录出现压缩过的字体库example-font.subset.ttf

此时的字体文件已经相较于原来有了近乎 90% 的压缩。

进一步压缩

使用在线压缩工具 Cloudconvert ,将ttf文件转换成woff2文件(进一步压缩大小)