因网速太慢我把20M+的字体压缩到了几KB

于水增

故事背景

事情起源于之前做的海报编辑器,自己调试时无意中发现字体渲染好慢,第一反应就是网怎么变慢了,断网了?仔细一看才发现,淦!这几个字体资源咋这么大,难怪网速变慢了呢😁😁。

图片中的海报包含6种字体,其中最大的字体文件超过20M,而最长的网络加载时长已接近20s。所以海报实际效果图展示耗时太久,很影响用户体验。那就趁此机会跟大家聊聊 字体 这件小事。

字体文件为什么那么大?

🙋 DeepSeek同学来回答下大家:

这里所说的大体积的字体资源多数是指中文主要原因下边两点

  • 中文字符数量庞大 ,英文仅 26 个字母 + 符号,中文(全字符集)包含 70,000+ 字符
  • 字形结构复杂,字体文件需为每个字符存储独立的矢量轮廓数据,而汉字笔画复杂,每个字符需存储数百个控制点坐标(例如「龍」字的轮廓点数量可能是「A」的 10 倍以上)

总结下来就是咱们不光汉字多,书法也是五花八门,它是真小不了。如果你硬要压缩,我们只能从第一点入手,将字符数量进行缩减,比如保留 1000 个常用汉字。

web网站中常见字体格式

由于我司物料部门提供的为TTF格式,所以这里通过 思源黑体 给一个直观的对比:

  • TTF 文件:16.9 MB
  • WOFF2 文件:7.4 MB(压缩率约 60%)

两者为什么会差这么多,其实WOFF2 只是在 TTF/OTF 基础上添加了压缩和 Web 专用元数据,且WOFF2支持增量解码,也就是边下载边解析,文本可更快显示(即使字体未完全加载,不过有待考证)。

TTF有办法优化吗?

回归问题本身

首先来简单回顾下我们自定义的字体是如何在浏览器中完成渲染的

一般情况下我们对字体文件的引用方式为下边三种

  • 通过绝对路径来引用,这种就是将字体文件打包在工程内,所以带来的结果就是工程打包文件体积太大
less 复制代码
@font-face {
  font-family: 'xxx';
  src: url('../../assets/fonts.woff2')
}
  • 第二种就是 CDN 中存放的字体文件,一般是通过这种方式来减少工程的编译后体积
less 复制代码
@font-face {
  font-family: 'xxx';
  src: url('https://xxx.woff2')
}
  • 通过 FontFace 构造一个字体对象

前两种一般是在浏览器构建 CSSOM 时,当遇到**<font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(243, 243, 243);">url()</font>**引用时会发起资源请求。第三种则是通过 js 来控制字体的加载流程,所以归根结底就是字体文件太大,导致网络资源下载速度慢,我们只能从优化字体大小的方向入手

确定解决方向

下面汇总下查到的具体几个优化方案,诸如提高网络传输效率,增加缓存之类的就不讲了,能够立竿见影的主要下边这两个方案

方案 方法/原理 适用场景
字体子集化 通过工具将字体文件进行提取(支持动态 ),返回指定的字符集的字体文件,其根本就是减少单次资源请求的体积,需要服务端支持 这个方案是所有优化场景的基础
按需加载 通过设置 unicode-range 属性,浏览器在进行css样式计算时候,会根据页面中的字符与设置的字符范围进行比对,匹配上会加载对应的字体文件 前提是资源已经被子集化,比较适用多语言切换的场景

简单来说,字体子集化 可单独食用,按需加载 则必须要将字体前置子集化。才能完美实现按需加载。就我的这个项目而言,动态子集化方案不要太完美,毕竟一张海报本身就没几个字儿!所以我们这次将抛弃 CDN,通过动态的将服务本地中的字体资源子集化来实现字体的压缩效果。

这里我们使用python 中的一个字体工具库 fontTools 来实现一个动态子集化,类似于 Google Fonts 的实现。核心思路就是将字符传给服务端,通过工具将传入的字符在本地字体文件中提取并返回给客户端,通过fontTools 还可以将TTF格式转化为和Web更搭的WOFF2格式。实现细节如下述代码所示

python 复制代码
@app.route('/font/<font_name>', methods=['GET'])
def get_font_subset(font_name):
    # 获取本地字体文件路径
    font_path = os.path.join(FONTS_DIR, f"{font_name}.ttf")
    # 获取子集字符
    chars = request.args.get('text', '')
    # 字体文件格式
    format = request.args.get('format', 'woff2').lower()

    # 处理字符,去重
    unique_chars = ''.join(sorted(set(chars)))
    try:
        # 配置子集化选项
        options = Options()
        options.flavor = format if format in {'woff', 'woff2'} else None
        options.desubroutinize = True  # 增强兼容性
        subsetter = Subsetter(options=options)
        
        # 加载字体并生成子集
        font = TTFont(font_path)
        subsetter.populate(text=unique_chars)
        subsetter.subset(font)

        # 保存为指定格式
        buffer = io.BytesIO()
        font.save(buffer)
        buffer.seek(0)

        # 确定MIME类型
        mime_type = {
            'woff2': 'font/woff2',
            'woff': 'font/woff',
        }[format]

        # 创建响应并设置
        response = Response(buffer.read(), mimetype=mime_type)
        # 其他设置...
        return response

    except Exception as e:
        # 子集化失败...

前端代码中增加了一些字符提取的工作,我本身就是通过 FontFace Api 来请求字体资源的,所以我仅需将资源链接替换为子集化字体的接口就可以了,下面代码来描述字体的加载过程

typescript 复制代码
// ...其他逻辑
Toast.loading('字体加载中')
// 遍历海报中的字体对象
[...new Set(fontFamilies)].forEach((fontName) => {
  // 在字体库中找到对应字体详细信息
  const obj = fontLibrary.find((el) => el?.value === fontName) ?? {};

  if (obj.value && obj.src) {
    // 处理海报中提取的文案集合
    const text = textMap[obj.value].join('');
    // 构建字体对象
    const font = new FontFace(
      obj.value,
      `url(http://127.0.0.1:5000/font/${obj.value}?text=${text}&format=woff2)`
    );
    // 加载字体
    font.load();
    // 添加到文档字体集中
    document.fonts.add(font);
  }
});
// 文档所有字体加载完毕后返回成功的 Promise
return document.fonts.ready.finally(() => Toast.destory());

好了,刷新下浏览器,来看看最终的效果:

这这 真立竿见影(主要是基数大😁😁),最终得到的结果就是,实际 22.4M 的字体文件,子集化后缩减到 3.6KB 。实际效果图生成的时间由 20s+ 缩减到毫秒级(300ms 以内)。这下就无惧网速了吧!

结语

总的来说,优化字体加载的方案有很多,我们需要结合自己的实际业务场景来进行选型,字体子集化确实是一种高效且实用的优化手段,更多的实践思路可以参考下 Google fonts

相关推荐
今天也想MK代码4 小时前
ReFormX:现代化的 React 表单解决方案 - 深度解析与最佳实践
前端·react.js·性能优化
yanyu-yaya4 小时前
第三章 react redux的学习之redux和react-redux,@reduxjs/toolkit依赖结合使用
javascript·学习·react.js
dleei4 小时前
react入门(上)
前端·react.js·前端框架
哟哟耶耶5 小时前
React-02初学hello_react(JSX,创建React根节点,引入对应React库,render渲染DOM)
前端·javascript·react.js
lbh5 小时前
React + PDF.js 实战:构建一个带打印/下载功能的 PDF 查看器
javascript·react.js
fightingles6 小时前
写给自己看的React注意事项
react.js
代码小学僧6 小时前
如何优雅地重构一个企业官网 Nextjs 前端项目
前端·react.js·前端工程化
三小河6 小时前
自定义VITE插件,生成可修改配置项,用于不同项目部署
前端·javascript·react.js
小钰能吃三碗饭11 小时前
第一篇:【前端翻身计划】从菜鸟到高手,JavaScript ES6+实战秘籍揭秘!
前端·javascript·react.js
蓉妹妹12 小时前
AntDesign下,Select内嵌Menu标签,做一个多选下拉框,既可以搜索,还可以选择下拉项
前端·react.js