把 16MB 中文字体压到 400KB:我写了一个 Vite 字体子集插件

一个真实的前端项目里,我遇到一个很常见却又容易被忽视的问题:一套中文界面,为了保证视觉效果引入了整套中文字体文件,结果单字体就占了十几 MB,构建产物和安装包体积都被严重拖累。现有方案要么依赖运行时,要么在工程化集成上不太符合我的需求。于是我决定写一个专门面向 Vite 的字体子集化插件 @fe-fast/vite-plugin-font-subset:在构建阶段自动收集实际用到的字符集,生成子集化后的 woff2 字体,并无缝替换原有资源,不侵入业务代码。在这篇文章里,我会分享这个插件诞生的背景、设计目标、关键实现思路,以及在真实项目中带来的体积优化效果,希望能给同样被"中文字体体积"困扰的你一些参考。

一、项目背景:16MB 的中文字体,把包体积拖垮了

我日常主要做的一个项目,是基于 Vue3 + Vite + Pinia + Electron 的桌面应用(电力行业业务系统)。

这个项目有两个典型特点:

  • 中文界面 + 大量业务术语:几乎所有页面都是中文,且有不少专业名词
  • 离线/弱网场景:不仅要打成 Electron 安装包,还要支持在弱网环境下更新

随着功能越来越多,我开始频繁在构建日志里看到这样一段"刺眼"的内容:

  • src/SiYuanHeiTi 目录里有两份 SourceHanSansCN OTF 字体
  • 每一份大概 8MB+ ,加起来就是 16MB+ 的纯字体资源

哪怕我已经做了一些优化:

  • 图片用 vite-plugin-imagemin 压缩
  • 代码做了基础的拆包和懒加载

构建产物里字体资源仍然是绝对大头

简单说:用户只是打开一个中文界面,却要被迫下载完整一套 GBK 字库,这显然太浪费了。

二、降体积的几种思路,对比一下

在真正动手写插件之前,我先把可能的方案都过了一遍,权衡了一下利弊。

方案 1:换成系统字体 / 常见 Web 字体

  • 优势
    • 不需要额外的字体文件,体积几乎为 0
  • 劣势
    • 设计同学辛苦做的 UI 风格会被破坏
    • 跨平台(Windows/macOS)渲染效果不可控,特别是复杂表格、图形界面
  • 适用场景
    • 对视觉统一要求不高的后台系统、管理台
  • 实现难度:⭐

方案 2:直接引入现成的字体子集化工具 / 在线服务

  • 优势
    • 现有方案成熟,不用自己"造轮子"
  • 劣势
    • 有些是在线服务,不适合公司内网/离线场景
    • 一些工具只关注命令行,不关注 Vite 构建流程 的无缝集成
  • 适用场景
    • 纯 Web 项目、对 CI/CD 环境更自由的团队
  • 实现难度:⭐⭐⭐

方案 3:使用已有的 Vite 字体子集插件

我也尝试过社区已有的 vite-plugin-font-subset 等插件,但踩到了两个坑:

  1. ESM-only 与现有工程的兼容问题
    • 有的插件是纯 ESM 包,而我当时的构建链路里,

      vite.config.js 仍然是以 CJS 方式被 esbuild 处理

    • 直接 import 会在加载配置阶段就报:

      ESM file cannot be loaded by require

  2. "大量中文 + 特殊字符"场景需要更多可配置性
  • 优势
    • 理论上"开箱即用",几行配置就能跑
  • 劣势
    • 在我的项目环境里,兼容性和可扩展性都有一些限制
  • 适用场景
    • Node / Vite 配置已经完全 ESM 化的新项目
  • 实现难度:⭐⭐

推荐选择 & 我的决策

  • 在综合权衡之后,我选择了:
    "在 Vite 插件体系内,写一个适配自己项目的字体子集化插件,并抽象成通用插件发布出来"

  • 于是就有了今天的这个包:

    **

    fe-fast/vite-plugin-font-subset**

三、我给插件定下的几个目标

在真正敲代码之前,我给这个插件定了几个很具体的目标:

  1. 零运行时开销

    • 所有工作都在 vite build 阶段完成
    • 运行时只加载子集后的 woff/woff2 文件
  2. 对现有项目"侵入感"足够低

    • 只需要在 vite.config 里增加一个插件配置
    • 不要求你改动业务代码里的 font-family 或静态资源引用方式
  3. 兼容我当前的工程形态

    • 支持 Electron + Vite 的场景
    • 避免"ESM-only 插件 + CJS 配置"这种加载失败问题
  4. 默认就能解决"中文大字体"问题

    • 在不配置任何参数的情况下,对于常规的中文页面,能直接减掉大部分无用字形

四、核心思路:从字符集到子集字体的流水线

具体实现细节线上可以看源码,这里更侧重讲清楚"思路",方便大家自己扩展或实现类似插件。

整个插件的执行链路,大致可以拆成四步:

1. 收集可能会用到的字符集

  • 扫描构建产物(或者源码)里的:
    • 模板中的中文文案
    • 国际化文案 JSON
    • 常见 UI 组件中的静态字符
  • 做一些去重和过滤,得到一个 相对完整但不过度膨胀的字符集合

这里的关键是平衡:

  • 集合太小:生产环境会出现"口口口/小方块"
  • 集合太大:子集化收益会变差

2. 调用子集化引擎生成子集字体

  • 将"原始 OTF/TTF 字体文件 + 上面的字符集"交给子集工具
  • 输出一份或多份新的字体文件(优先 woff2)

在我的项目中,最终生成的结果类似构建日志中的这一行:

scss 复制代码
textSourceHanSansCN-Normal-xxxx.woff2    223.97 kBSourceHanSansCN-Medium-xxxx.woff2    224.79 kB

相比最初 两份 8MB+ 的 OTF 文件,体积已经被压到了大约十分之一左右。

3. 更新 CSS / 资源引用

  • 在原有的

    font-face 声明基础上,修改 src 指向子集化后的文件

  • 对于 Vite 生成的静态资源目录(如 dist/prsas_static),保持输出路径稳定,避免破坏现有引用

这一部分的目标是:对业务代码完全透明,你仍然可以这样写:

css 复制代码
cssbody {  font-family: 'SourceHanSansCN', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;}

只是最终加载的资源,不再是原来那两个 8MB 的 OTF,而是几百 KB 的子集 woff2。

4. 和 Vite 构建流程集成

  • 通过 Vite 插件 API,在合适的生命周期(如 configResolvedgenerateBundle 等)
    • 拿到最终输出目录
    • 触发上面的子集化流水线
    • 将生成的文件写回到 Rollup 构建产物中

核心原则就是:不打破 Vite 原有工作流,只是"在尾部插一个子集化步骤"

五、在真实项目中的效果

以我这个 Electron + Vite 项目为例,启用

fe-fast/vite-plugin-font-subset 之后:

  • 原来两份 8MB+ 的 OTF 中文字体
  • 变成两份 两百多 KB 的 woff2 子集字体
  • 对比结果非常直观:
    • 安装包体积明显下降
    • 首次加载速度、增量更新速度都有肉眼可见的提升
    • 用户几乎感受不到视觉上的差异

配合 vite-plugin-imagemin 对 PNG 等图片资源的压缩,整体构建体验也变成了:

  • 构建时间长一点(多了字体子集化和图片压缩),但属于"可接受的离线计算"
  • 换来的是 更小的安装包、更快的首屏体验,尤其适合弱网和内网环境

六、如何使用这个插件

简单说一下使用方式(仅作示意,具体参数可以看 README):

css 复制代码
bashnpm install -D @fe-fast/vite-plugin-font-subset

ts// vite.config.ts / vite.config.jsimport fontSubsetPlugin from '@fe-fast/vite-plugin-font-subset'export default defineConfig({  plugins: [    vue(),    fontSubsetPlugin({      // 一些可选配置,例如:      // fonts: [{ path: 'src/SiYuanHeiTi/SourceHanSansCN-Normal.otf', name: 'SourceHanSansCN' }],      // include: ['src/**/*.vue', 'src/**/*.ts'],      // ...    }),  ],})

做到这一点之后,剩下的事情就交给构建阶段处理即可。

七、过程中的几个坑 & 经验

在开发这个插件的过程中,也遇到了一些值得记录的坑:

  • ESM vs CJS 的兼容

    • 之前用其他字体插件时,遇到过 ESM file cannot be loaded by require 的报错
    • 这直接促使我在发布这个插件时,特别注意构建目标和导出形式,让它能更好地兼容现有工程
  • 字符集过于激进会导致"缺字"

    • 一开始我只统计了模板里的中文字符,结果线上发现某些动态内容会出现"口口口"
    • 最终方案是:适度保守 + 预留一部分常用汉字范围
  • 构建时间和体验的平衡

    • 字体子集化本身是一个"CPU 密集型"的过程
    • 在开发环境我默认关闭了子集化,仅在 vite build 时启用,保证日常开发体验

八、总结与展望

fe-fast/vite-plugin-font-subset 其实不是一个"炫技"的轮子,而是从真实业务需求里长出来的:

  • 它解决的是一个非常具体的问题:中文项目中,字体资源过大导致包体积和加载体验变差
  • 它也体现了我在做前端工程化时的一些偏好:
    • 用好现有工具链(Vite 插件体系)
    • 优先选择"构建时处理",而不是在运行时增加复杂性
    • 遇到兼容性问题时,适当地"自己造一个更适合现有工程的轮子"

后续我还希望在这个插件上做几件事:

  • 更智能的字符集分析(结合路由拆分、按需子集)
  • 提供简单的可视化报告,让你一眼看到"字体减肥"前后的体积对比
  • 增强对多语言项目的支持
相关推荐
林太白2 小时前
跟着TRAE SOLO全链路看看项目部署服务器全流程吧
前端·javascript·后端
humor2 小时前
Quill 2.x 从 0 到 1 实战 - 为 AI+Quill 深度结合铺路
前端·vue.js
先生沉默先2 小时前
NodeJs 学习日志(8):雪花算法生成唯一 ID
javascript·学习·node.js
起这个名字2 小时前
Webpack——插件实现的理解
前端·javascript·node.js
二川bro3 小时前
第51节:Three.js源码解析 - 核心架构设计
开发语言·javascript·ecmascript
djk88884 小时前
多标签页导航后台模板 html+css+js 纯手写 无第三方UI框架 复制粘贴即用
javascript·css·html
Hilaku5 小时前
别再吹性能优化了:你的应用卡顿,纯粹是因为产品设计烂🤷‍♂️
前端·javascript·代码规范
驯狼小羊羔5 小时前
学习随笔-hooks和mixins
前端·javascript·vue.js·学习·hooks·mixins
麦麦大数据6 小时前
F048 体育新闻推荐系统vue+flask
前端·vue.js·flask·推荐算法·体育·体育新闻