现代Web开发中的中文字体处理——我就想省点流量!
在现代网页中,文字是信息传递的核心载体,而字体则是其中一个非常重要的向用户展示网站风格的途径。对于英文网站,我们通常可以放心地使用系统默认字体(如Arial等),跨平台的稳定性表现通常很好。然而,当网站需要处理大量中文时,很多时候就需要做一些特殊的处理了。
最大的问题就是默认中文字体在不同系统上通常风格迥异。以无衬线字体为例,Windows通常使用的是微软雅黑(Microsoft YaHei),MacOS则钟爱苹方(PingFang SC),而Linux则多热衷于文泉驿微米黑或思源黑体等开源字体,各个手机系统更是纷纷使用自家厂商推出的字体。这导致中文网站若使用系统默认字体渲染,很容易导致跨平台视觉风格不统一,甚至会出现排版错乱等严重设计问题。因此,一般比较建议显示中文网页时为用户提供特定的网络字体。
但这又会带来新的问题:中文字体文件对于网络传输过于庞大了。与很多只需处理不到128个的ASCII字符的英文字体不同,中文字体需要覆盖的字符数量及其庞大,最基本的的GB2312字符集即包含了6763个汉字,完整的GBK更是超过了2万个。以本站使用的更纱黑体(Sarasa UI SC)为例,作为一个出名的"大字库",仅Regular一个字重,其未经压缩的TTF文件就达到了12MB,在并发请求时对服务器会造成极大的带宽压力,且客户端也会感受到明显的渲染延迟或字体跳变。
最简单的方法是:使用工具,将原始TTF文件转化为使用Brotli算法压缩的WOFF2格式。例如更纱黑体的WOFF2版本,同样是Regular一个字重,同样是包含48756个字符的大型中文字体,体积直接降到了4.9MB,带宽压力降低了一半还多。

或许你可以试试我的字体转换工具,支持TTF/OTF与WOFF2的相互转换~
然而,即使经过了压缩,MB级别的文件在弱网或未配置CDN的情况下仍然会显著影响用户体验。为此,我们可以使用第二个处理方式:使用字体分片。
虽然完整的字体文件包含数万个字符,但一个网页用到的往往只有几百个,标题或封面页甚至可能只会用到几十个。因此,为了加快网络传输速度,我们完全可以只传输网页真正用到了的字符。例如,可以利用Python的fonttools包方便地提取需要的某些字符:
# 安装 fonttools
pip install fonttools zopfli
# 提取"首页关于我们联系"这几个字并输出为 woff2
pyftsubset SarasaUISC-Regular.ttf --text="首页关于我们联系" --flavor=woff2 --output-file=sarasa-mini.woff2
这样提取后的文件可能只有几十KB,大大降低了网络压力。
但是,这个方案需要提前提取网站上的文字,如果是用户发表的文章/评论,无法预先知道需要显示的内容,又该怎么办呢?这种情况下,我们可以选用一个堪称绝妙的方法——基于字频的预分片。如果说传统的Web中文字体是“强行让用户扛着大米袋子上楼”,那预分片就是“每次只给用户装今天想吃的那一碗”。
推荐一个基于node.js的小工具(你可能需要先安装node环境):cn-font-split,它使用了基于语料库统计的字频与关联切分算法,可以实现将高频字集中打包,相关字关联打包的效果,尽可能降低浏览器请求的分片数量,最大化地降低网络压力:
npx cn-font-split -i /path/to/your/font.woff2 -o /path/to/your/font/outputdir
在输出文件夹中,原有的字体会被按优化过的算法切分为若干个小文件,同时生成可以直接使用的result.css用于教浏览器将每个字符与其所在的文件对应起来。这样,浏览器将只会请求网页中需要渲染的文字所在的几个分片,也就可以做到在不预先知道网页内容的前提下达到节省带宽的效果了。但与此同时,分片方案也会产生一个新的问题:对于文字比较多的页面,浏览器可能需要请求比较多的小分片,对于HTTP/1.1协议,这可能会导致比较严重的"队头阻塞"问题。因此,采用该方案时强烈建议开启HTTP/2或HTTP/3协议,它们对多个小文件请求的场景做了优化,通常会很有帮助。
综上,如果是有大量用户生成内容的网站,首选字体分片方案配合HTTP/2;如果是追求最佳打开速度的静态页面,则推荐使用 Python预先提取需要的字形;如果是内部网站或对首屏渲染速度要求不高,则直接使用WOFF2格式的字体是最为简单的选择。希望本文能帮你打破中文字体加载的桎梏,让每一个汉字都能在你的网页上轻盈、快速、准确地呈现。