本文最后更新于 2024年6月18日 下午
问题分析 自定义字体文件加载过慢,导致网站设定的字体迟迟无法显示。 由于我个人比较重视网站的整体设计,因此对于网站字体有着较高的要求。为了保证网站字体美观(与主题适配、方便阅读)、统一(在不同平台上均为统一字体,展现的效果一致),我选择了上传自定义字体文件(css中的@font-face方法)作为网站资源,并在用户访问网站时加载字体文件用于显示。相关css设置如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @font-face { font-family : "Normal" ; src : url ("/font/Normal.ttf" ) format ('truetype' ); }@font-face { font-family : "Heavy" ; src : url ("/font/Heavy.ttf" ); }html , body , p { font-family : "Normal" , "新宋体" , sans-serif, Georgia; }.h1 , .h2 , .h3 , .h4 , .h5 , .h6 , h1 , h2 , h4 , h4 , h5 , h6 , strong { font-family : "Heavy" , "新宋体" , sans-serif, Georgia; font-weight : bolder; }
其中的Normal.ttf与Heavy.ttf均为思源宋体(分别是Semibold与Heavy)。 如此配置后,的确可以满足上述对于字体的“美观、统一”的需求,但出现了加载效率的问题。由于字体文件体积较大(均为约10MB),网站大部分内容加载出来后,字体仍然无法下载完成并进行显示,导致原有的设计效果无法正常呈现给用户。根据此问题,对网站字体提出新的需求——“快速”。 要实现“快速”的效果,大致有两个方向思路:1.加快加载速度(提高连接速度/字体文件并行加载)2.压缩字体文件体积。根据目前的个人能力水平,我选择从后者寻找方法。
解决方案 压缩字体文件体积的主要思路就是“去除无用,留下有用”。一个字体文件需要存储大量的数据以保证每一个字(无论是否生僻)都能有相对应的字形,然而网站中大概率不会把所有字都使用一遍。因此,如果可以根据网站的文字内容,留下字体文件中需要用到的字的数据并删除其余的冗余数据,就可以实现压缩体积的目的。最终实践呈现出的效果是,MB级别的字体文件可以被压缩至KB级别,大大缩小了字体文件,满足了“快速”的需求。 事实上,已经有现成的产品font-spider/font-spider-plus能够解决这类问题。但我在应用的过程中遇到了一些难以解决的问题(运行font-spider后,成功检索了需要用到的字但未能完成冗余数据删除工作),这个问题可能和字体文件格式/字体文件内部存储方式有关,因此放弃了现成的解决方案。
压缩字体文件体积,其实就是取字体文件数据的一个子集。python的fonttools库提供了subset方法,能够实现取一个字体文件(如.ttf格式)的子集。fonttools可以直接作为命令在命令行中使用,其格式大致如下:
1 fonttools subset A --text-file=B --output-file=C
其中,A为原始字体文件,C为压缩后的字体文件。B则是一个txt文件,他用来告知fonttools有哪些字需要被使用(A子集中有哪些元素)。 下面是一个简单的例子:
1 fonttools subset source.ttf --text-file=use.txt --output-file=result.ttf
其中use.txt存储的内容为“你好世界”。那么该指令运行结束后就会生成一个result.ttf字体文件,这个文件只存储了“你、好、世、界”这四个字的字体数据,文件体积大大减小。
全站文件检索 上述内容已经为达成压缩字体文件的需求奠定了技术基础。下面考虑,如何得到use.txt这个文件?换言之,如何得到这个网站中需要用到的所有字是哪些? hexo 生成的网站中展示给用户的是内容均为.html文件,并且所有网站需要使用到的资源(包括.html文件)均存储在public文件夹中,因此,我们只需要遍历整个public文件夹并读取所有的html文件内容,将其中使用到的字符进行登记、去重,最终保存到use.txt文件中即可。需要注意的是,public文件夹中存在着文件夹的嵌套(文件夹内包含文件夹),并且文件夹的深度并不唯一(可能存在多重嵌套),为了保证遍历到每一层、每一个文件夹中的每一个文件,可以选择使用深度优先搜索/广度优先搜索的方法进行遍历。
实现代码 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 58 59 60 61 62 63 64 from pathlib import Pathimport osdef is_chinese_char (char ): return '\u4e00' <= char <= '\u9fff' def is_special_char (char ): return not char.isalnum() and not char.isspace()def process_html_files (folder_path ): queue = [Path(folder_path)] chinese_chars = [] special_chars = [] with open ('result.txt' , 'w' , encoding='utf-8' ) as result_file: while queue: current_folder = queue.pop(0 ) for item in current_folder.iterdir(): if item.is_file() and item.suffix == '.html' : with item.open ('r' , encoding='utf-8' ) as html_file: content = html_file.read() for char in content: if is_chinese_char(char) and char not in chinese_chars: chinese_chars.append(char) elif is_special_char(char) and char not in special_chars: special_chars.append(char) elif item.is_dir(): queue.append(item) for i in chinese_chars: result_file.write(i) for i in special_chars: result_file.write(i) for i in range (ord ('z' ) - ord ('a' ) + 1 ): result_file.write(chr (i + ord ('a' ))) result_file.write(chr (i + ord ('A' ))) for i in range (10 ): result_file.write(chr (i + ord ('0' ))) folder_path = '.../blog/public' process_html_files(folder_path) Heavy_font_path = '.../blog/public/font/Heavy.ttf' Normal_font_path = '.../blog/public/font/Normal.ttf' Temp_font_path = '.../blog/public/font/temp.ttf' os.system("fonttools subset " + Heavy_font_path + " --text-file=result.txt --output-file=" + Temp_font_path)if os.path.exists(Heavy_font_path): os.remove(Heavy_font_path)if os.path.exists(Temp_font_path): os.rename(Temp_font_path, Heavy_font_path) os.system("fonttools subset " + Normal_font_path + " --text-file=result.txt --output-file=" + Temp_font_path)if os.path.exists(Normal_font_path): os.remove(Normal_font_path)if os.path.exists(Temp_font_path): os.rename(Temp_font_path, Normal_font_path)