微信公众号自定义菜单支持配置小图标,但平台对图标有明确的限制:必须是JPG或PNG格式且大小不超过128KB,如果用SVG则往往通过Base64编码内嵌或者转换为其他方式使用,而更多的场景是H5页面、小程序内嵌页面中直接引用SVG图标。不管哪种使用方式,设计软件直接导出的SVG文件几乎都存在大量冗余:编辑器元数据、无用的注释、默认属性值、过高的数值精度,这些都会让文件体积膨胀数倍。本文就带大家动手开发一个针对公众号场景的SVG优化工具,把SVG代码压缩到最精简的状态。

一、先看看SVG文件里到底藏了多少冗余
以Figma、Sketch或者Illustrator导出的SVG为例,一个简单的图标文件打开后往往会看到类似这样的内容:
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="48px" height="48px"
viewBox="0 0 48 48" enable-background="new 0 0 48 48"
xml:space="preserve" id="Layer_1" version="1.1">
<!-- Generator: SomeDesignTool 24.0 -->
<title>icon</title>
<desc>Created with a design tool</desc>
<metadata>...大量编辑器私有数据...</metadata>
<path d="M12.0000001 24.0000003 L36.0000004 24.0000001" fill="#333333"
stroke="none" stroke-width="1"/>
</svg>仔细分析可以发现冗余主要集中在几个方面。首先是编辑器生成的元数据,包括<metadata>、<title>、<desc>以及注释,这些对渲染毫无作用,却能占据几百字节。其次是默认属性值,比如stroke="none"本身就是SVG的默认行为,写了等于没写。再者是数值精度问题,设计工具为了保证视觉精度会把坐标写到小数点后七八位,而图标尺寸通常只有几十像素,保留两位小数已经绰绰有余。最后还有一些完全无用的属性,比如version、xml:space、enable-background等,浏览器根本不认识或者早已废弃。
一个2KB的SVG经过清理后经常能压到500字节以内,压缩率可达70%以上。如果图标需要以Base64形式嵌入页面,体积缩小意味着更小的HTTP响应和更快的首屏渲染,这在移动端公众号H5场景中尤其重要。
二、优化工具的整体设计与核心算法
工具采用Node.js开发,核心思路是把SVG当作XML解析成DOM树,再按规则逐节点清洗。之所以不直接用正则替换,是因为SVG的属性可能出现在多种上下文中,正则处理容易误伤有效内容,比如路径数据d属性中同样会出现引号和数字。这里选择用svgo的思想自己实现一个精简版,方便针对公众号场景做定制。
整体流程分四步:解析XML生成语法树、遍历节点删除无用元素和属性、压缩路径数据中的数值精度、重新序列化输出。下面是核心代码框架:
const { parse } = require('svg-parser');
// 需要整体删除的标签
const REMOVE_TAGS = ['title', 'desc', 'metadata', 'comment', 'defs'];
// 可以安全删除的属性(浏览器使用默认值即可)
const REMOVE_ATTRS = [
'version', 'xmlns:xlink', 'xml:space', 'enable-background',
'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin',
'id', 'class'
];
function optimize(svgSource) {
const ast = parse(svgSource);
walk(ast.children[0]);
return serialize(ast);
}
// 递归遍历节点,清理属性和子节点
function walk(node) {
if (node.type === 'element') {
if (REMOVE_TAGS.includes(node.name)) {
node.remove = true;
return;
}
for (const attr of Object.keys(node.attributes)) {
if (REMOVE_ATTRS.includes(attr)) {
delete node.attributes[attr];
}
// 删除值为默认值的属性
if (node.attributes[attr] === 'none' && attr === 'stroke') {
delete node.attributes[attr];
}
}
node.children.forEach(walk);
}
}
module.exports = { optimize };属性清理环节有几个细节值得注意。xmlns命名空间声明必须保留,否则SVG无法独立渲染;但如果SVG准备内联到HTML中使用,xmlns也可以去掉。删除defs要谨慎,需要先确认里面没有被引用的渐变或裁剪路径,简单做法是扫描全文是否存在url(#id)引用,没有引用才删除。此外id属性的删除也有同样问题,被use元素引用的id不能删。
三、数值精度压缩与路径数据优化
数值精度压缩是体积收益最大的一环。路径的d属性形如M12.0000001 24.0000003L36.0000004 24.0000001,我们需要把每个数字四舍五入到指定精度,并去掉整数部分末尾多余的零和小数点。实现代码如下:
// 精度压缩:12.0000001 -> 12,24.34999999 -> 24.35
function roundNum(num, precision = 2) {
let s = Number(num).toFixed(precision);
s = s.replace(/\.?0+$/, ''); // 去掉尾部多余的0
return s === '-0' ? '0' : s;
}
function compressPath(d, precision) {
// 匹配路径中的所有数字,逐个压缩
return d.replace(/-?\d*\.?\d+(?:e[-+]?\d+)?/gi, (m) => {
return roundNum(m, precision);
});
}
// 进一步优化:命令字母前如果是逗号或空格可以省略分隔符
function compactPath(d) {
return d
.replace(/\s*([MLHVCSQTAZmlhvcsqtaz])\s*/g, '$1')
.replace(/\s+/g, ' ')
.trim();
}除了精度压缩,还可以做命令化简。水平线用H代替L,垂直线用V代替L,坐标值与前一个命令相同时直接省略。更进一步,如果工具的使用者愿意接受轻微的形状偏差,可以合并距离极近的连续点,比如两个点距离小于0.3就视为同一点,这样能删掉不少冗余节点。对于图标类图形,这种偏差在人眼上完全不可分辨。
精度参数建议默认设为2。可以做个简单实验:一个包含二十多个路径段的图标,精度从7降到2,体积从1180字节降到460字节,而渲染效果在48x48的显示尺寸下没有任何可见差异。精度设为1虽然能再省一点体积,但圆形路径会出现明显锯齿,不建议。
四、序列化输出与工具封装
清理完成后需要把语法树重新序列化成字符串。序列化时不要换行和缩进,所有属性用双引号包裹,相邻标签之间不加任何空白:
function serialize(node) {
if (node.remove) return '';
if (node.type === 'text') return node.value.trim();
const attrs = Object.entries(node.attributes)
.map(([k, v]) => `${k}="${v}"`)
.join(' ');
const children = node.children.map(serialize).join('');
return `<${node.name}${attrs ? ' ' + attrs : ''}>${children}</${node.name}>`;
}最后把工具封装成命令行程序,支持批量处理整个目录下的SVG文件,还可以顺带输出优化前后的体积对比:
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { optimize } = require('./optimize');
const dir = process.argv[2] || './icons';
fs.readdirSync(dir).filter(f => f.endsWith('.svg')).forEach(file => {
const full = path.join(dir, file);
const before = fs.readFileSync(full, 'utf8');
const after = optimize(before);
fs.writeFileSync(full, after);
const saved = (1 - after.length / before.length) * 100;
console.log(`${file}: ${before.length} -> ${after.length} 字节,节省 ${saved.toFixed(1)}%`);
});封装成命令行工具后,配合npm scripts在构建流程中自动执行,设计师提供的图标文件一进入项目就会被自动压缩,无需人工干预。如果还想进一步减小传输体积,可以在这之上叠加gzip,SVG文本内容重复度高,gzip后通常还能再压缩一半以上。
整个工具从分析冗余来源到实现清理、压缩、序列化,核心代码不到两百行,却能给公众号项目中的图标资源带来实实在在的瘦身效果。有兴趣的话还可以在此基础上扩展雪碧图合并、自动生成Base64内联样式等功能,让图标管理更加自动化。