一个真实的前端项目里,我遇到一个很常见却又容易被忽视的问题:一套中文界面,为了保证视觉效果引入了整套中文字体文件,结果单字体就占了十几 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 等插件,但踩到了两个坑:
- ESM-only 与现有工程的兼容问题
-
有的插件是纯 ESM 包,而我当时的构建链路里,
vite.config.js 仍然是以 CJS 方式被 esbuild 处理
-
直接
import会在加载配置阶段就报:ESM file cannot be loaded by
require
-
- "大量中文 + 特殊字符"场景需要更多可配置性
- 优势
- 理论上"开箱即用",几行配置就能跑
- 劣势
- 在我的项目环境里,兼容性和可扩展性都有一些限制
- 适用场景
- Node / Vite 配置已经完全 ESM 化的新项目
- 实现难度:⭐⭐
推荐选择 & 我的决策:
-
在综合权衡之后,我选择了:
"在 Vite 插件体系内,写一个适配自己项目的字体子集化插件,并抽象成通用插件发布出来"。 -
于是就有了今天的这个包:
**
fe-fast/vite-plugin-font-subset**
三、我给插件定下的几个目标
在真正敲代码之前,我给这个插件定了几个很具体的目标:
-
零运行时开销
- 所有工作都在
vite build阶段完成 - 运行时只加载子集后的 woff/woff2 文件
- 所有工作都在
-
对现有项目"侵入感"足够低
- 只需要在
vite.config里增加一个插件配置 - 不要求你改动业务代码里的
font-family或静态资源引用方式
- 只需要在
-
兼容我当前的工程形态
- 支持 Electron + Vite 的场景
- 避免"ESM-only 插件 + CJS 配置"这种加载失败问题
-
默认就能解决"中文大字体"问题
- 在不配置任何参数的情况下,对于常规的中文页面,能直接减掉大部分无用字形
四、核心思路:从字符集到子集字体的流水线
具体实现细节线上可以看源码,这里更侧重讲清楚"思路",方便大家自己扩展或实现类似插件。
整个插件的执行链路,大致可以拆成四步:
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,在合适的生命周期(如
configResolved、generateBundle等)- 拿到最终输出目录
- 触发上面的子集化流水线
- 将生成的文件写回到 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 插件体系)
- 优先选择"构建时处理",而不是在运行时增加复杂性
- 遇到兼容性问题时,适当地"自己造一个更适合现有工程的轮子"
后续我还希望在这个插件上做几件事:
- 更智能的字符集分析(结合路由拆分、按需子集)
- 提供简单的可视化报告,让你一眼看到"字体减肥"前后的体积对比
- 增强对多语言项目的支持