3. 字体与图标压缩:字体子集化、SVG 图标合并与 Sprite 技术
字体和图标,往往是页面资源加载的「隐形杀手」。你想想看,一套完整的字体文件动辄几 MB,但实际用到的字符可能不到 1%。图标也是,一个页面几十个小图标,每个都单独发一次 HTTP 请求,那性能能好吗?
这一章,我们就来聊聊怎么给字体和图标「瘦身」。说白了,就是只保留你需要的,把多余的统统砍掉。
3.1 字体子集化:只保留你需要的字符
字体子集化,是我个人在项目中用得最多的优化手段之一。它的核心思路很简单:从完整的字体文件中,提取出你页面真正用到的字符,生成一个新的、更小的字体文件。
举个例子,你用了「思源黑体」做标题,但整个页面只用了 20 个汉字。那何必加载整个 5MB 的字体文件?子集化之后,可能就剩 50KB 了。
3.1.1 什么时候该做子集化?
- 页面内容固定:比如官网、活动页、产品介绍页,文字内容基本不变。
- 只用于标题或少量文字:正文用系统字体,标题用自定义字体。
- 字体文件过大:超过 500KB 的字体,子集化收益非常明显。
3.1.2 怎么做子集化?
我常用的工具是 fonttools 和 glyphhanger。前者是 Python 库,功能强大;后者是命令行工具,用起来更顺手。
这里我演示一下用 glyphhanger 的操作流程:
# 1. 先扫描页面,提取所有用到的字符
glyphhanger https://example.com --spider --css --json > used_chars.json
# 2. 根据提取的字符,生成子集字体
glyphhanger --subset=./original.ttf --US_ASCII --whitelist=甲乙丙丁戊己庚辛壬癸 --formats=woff2,woff
# 3. 输出结果:subset.woff2 和 subset.woff
嗯,这里要注意:--US_ASCII 会包含基本的英文字母和数字,--whitelist 则指定你需要的中文字符。如果你不确定页面用了哪些字,可以用 --spider 参数让工具自动爬取。
fontmin-webpack 插件,或者写一个简单的 Node 脚本,在打包时自动处理字体文件。这样每次发布都不用手动操作。
3.1.3 子集化后的字体格式选择
| 格式 | 压缩率 | 浏览器支持 | 我的建议 |
|---|---|---|---|
| WOFF2 | 最高(比 WOFF 小 30%-50%) | 现代浏览器均支持 | 首选,体积最小 |
| WOFF | 中等 | IE9+ 支持 | 作为 WOFF2 的降级方案 |
| TTF/OTF | 无压缩 | 全平台支持 | 不推荐用于 Web,仅做开发调试 |
| EOT | 低 | 仅 IE 支持 | 如果不需要兼容 IE8 以下,可以放弃 |
我在项目中通常只生成 WOFF2 和 WOFF 两种格式,然后用 @font-face 的 format() 提示浏览器优先加载 WOFF2。
@font-face {
font-family: 'MySubsetFont';
src: url('subset.woff2') format('woff2'),
url('subset.woff') format('woff');
font-weight: normal;
font-style: normal;
font-display: swap;
}
3.2 SVG 图标合并:从零散到统一
字体子集化解决的是字体文件的问题。那图标呢?以前大家喜欢用图标字体(比如 Font Awesome),但图标字体有个硬伤:它只能显示单色图标。对于需要多色、渐变、甚至动画的图标,SVG 才是更好的选择。
但 SVG 图标多了,每个都单独加载,HTTP 请求数就上去了。怎么办?合并。
3.2.1 什么是 SVG 图标合并?
简单说,就是把多个 SVG 图标放在一个文件里。每个图标用一个 <symbol> 或 <g> 包裹,并赋予唯一的 id。使用时,通过 <use> 标签引用对应的 id 即可。
这样做的好处很明显:
- 减少请求数:所有图标一次加载,后续直接使用。
- 可缓存:合并后的 SVG 文件可以设置长缓存时间。
- 支持多色和动画:比图标字体灵活得多。
3.2.2 如何合并 SVG 图标?
我常用的工具是 svg-sprite 和 svgstore。前者是 Webpack 插件,后者是 Node 库,适合在构建流程中使用。
这里我用 svg-sprite 举个例子:
// webpack.config.js
const SVGSpriter = require('svg-sprite');
module.exports = {
// ... 其他配置
module: {
rules: [
{
test: /\.svg$/,
use: [
{
loader: 'svg-sprite-loader',
options: {
extract: true,
spriteFilename: 'icons.svg'
}
}
]
}
]
}
};
配置好之后,在组件里直接引入 SVG 文件:
import iconHome from './icons/home.svg';
import iconUser from './icons/user.svg';
// 在模板中使用
<svg>
<use xlink:href="#home" />
</svg>
构建后,所有图标会被合并到一个 icons.svg 文件中,页面只需加载一次。
<use> 引用,减少 HTTP 请求,同时保留 SVG 的所有能力。
3.3 Sprite 技术:不只是图标
说到 Sprite,很多人第一反应是 CSS Sprite(雪碧图)。没错,那是把多个小图片合并成一张大图,通过 background-position 来定位。但 SVG Sprite 和它不太一样。
3.3.1 CSS Sprite vs SVG Sprite
| 对比项 | CSS Sprite(雪碧图) | SVG Sprite |
|---|---|---|
| 文件类型 | PNG/JPG 位图 | SVG 矢量图 |
| 缩放 | 放大模糊 | 无限缩放,清晰 |
| 颜色 | 固定,难以修改 | 可通过 CSS 控制颜色、渐变 |
| 动画 | 不支持 | 支持 CSS 动画和 SMIL 动画 |
| 使用方式 | background-position |
<use> 标签引用 |
| 维护成本 | 需要精确计算坐标 | 只需管理 SVG 文件 |
我个人更推荐 SVG Sprite。原因很简单:维护方便,效果更好。你想想看,CSS Sprite 每次新增或修改图标,都得重新拼图、重新计算坐标,太痛苦了。SVG Sprite 只需要把新的 SVG 文件丢进去,构建工具自动处理。
3.3.2 内联 SVG Sprite 的最佳实践
在实际项目中,我通常把 SVG Sprite 文件内联到 HTML 中,或者通过 JavaScript 动态注入。这样做的好处是:
- 零额外请求:Sprite 文件直接嵌入页面,不产生网络请求。
- 首屏可用:页面加载后,图标立即可用,没有加载延迟。
- 便于缓存:如果 Sprite 内容不变,可以放在公共模板中,利用浏览器缓存。
内联的方式很简单:
<!-- 在页面头部或底部嵌入 Sprite -->
<svg style="display: none;">
<symbol id="icon-home" viewBox="0 0 24 24">
<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>
</symbol>
<symbol id="icon-user" viewBox="0 0 24 24">
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
</symbol>
</svg>
<!-- 使用图标 -->
<svg class="icon">
<use href="#icon-home" />
</svg>
3.4 本章知识体系总览
为了让你更直观地理解这三部分的关系,我画了一张流程图:
3.5 避坑指南与实战经验
讲完了理论和方法,我来分享几个实际项目中踩过的坑。
3.5.1 字体子集化的坑
我曾经在一个多语言项目中做字体子集化,只提取了中文和英文的字符。结果上线后,用户反馈某些页面上的数字显示异常。排查了半天才发现,原来那些数字是「全角数字」,和普通的「半角数字」编码不同,没有被包含在子集中。
所以,做子集化时,一定要把可能出现的字符都考虑进去。比如:
- 全角/半角数字和标点
- 特殊符号(©、®、™ 等)
- 货币符号(¥、€、$ 等)
- 常见的 emoji(如果页面支持)
3.5.2 SVG Sprite 的坑
我记得有一次,我用 svg-sprite-loader 合并图标,结果发现某些图标在页面上显示不出来。打开控制台一看,报错了:Unsafe attempt to load URL。原来是 SVG 文件里引用了外部资源(比如一个图片),被浏览器的安全策略拦截了。
解决方案很简单:确保所有 SVG 图标都是自包含的,不要引用任何外部资源。如果图标里需要图片,直接把图片转成 base64 嵌入 SVG 中。
viewBox 属性不一致。如果合并的图标 viewBox 大小不同,显示时可能会出现偏移或缩放异常。建议所有图标统一使用相同的 viewBox(比如 0 0 24 24),或者在 <use> 时手动设置宽高。
3.5.3 性能权衡
字体子集化和 SVG Sprite 虽然能显著减少资源体积,但也不是万能的。你需要根据项目实际情况做权衡:
- 小项目:如果页面只有几个图标,直接内联 SVG 代码可能比做 Sprite 更简单。
- 大项目:如果图标数量超过 20 个,Sprite 的收益就很明显了。
- 动态内容:如果页面文字是动态生成的,字体子集化可能不适用,可以考虑用系统字体或可变字体。
嗯,说到底,优化的核心是「按需加载」。你不需要的东西,就不要让它出现在用户的浏览器里。这个道理,放在字体和图标上,再合适不过了。