Docker Desktop と SVGO プラグインでしている SVG 最適化について、覚書を残しておきます。
設定ファイル(imagemin.js)
imagemin.js
const keepfolder = require('imagemin-keep-folder');
const path = require('path');
const crypto = require('crypto');
const { JSDOM } = require('jsdom');
const fs = require('fs');
(async () => {
const imageminSvgo = (await import('imagemin-svgo')).default;
// Imageminで最初の圧縮・最適化
await keepfolder(['src/**/*.svg'], {
plugins: [
imageminSvgo({
// コード整形用プラグイン
// js2svg: {
// indent: 2,
// pretty: true,
// closeSelfClosingTag: true, // 自己閉じタグを保持する
// },
plugins: [
'removeDimensions', // 幅と高さを削除し、viewBox のみで制御
'removeXMLProcInst', // XML宣言を削除
'removeComments', // コメントを削除
'removeMetadata', // <metadata> タグを削除
'removeUselessDefs', // <defs> タグ内で使われていないものを削除
'collapseGroups', // グループ要素を一つにまとめる
'convertStyleToAttrs', // インラインのスタイルを対応するSVG属性に変換
'sortAttrs', // 属性をソート
'convertShapeToPath', // 図形を <path> タグに変換
'convertColors', // 色の表現方法を最も短い形式に変換
'convertPathData', // パスデータの圧縮
{
name: "removeAttrs", // 不要な属性を削除
params: {
attrs: 'svg:fill:none' // <svg> タグの fill="none" を削除
}
},
{
name: "inlineStyles", // <style> タグ内のスタイルをインライン化
params: {
onlyMatchedOnce: true,
removeMatchedSelectors: true
}
},
{
name: "cleanupIds", // 未使用IDを削除し、短くする
params: {
remove: true,
minify: true,
preserve: [],
preservePrefixes: [],
force: false
}
},
{
name: "addAttributesToSVGElement", // <svg> タグに属性を追加
params: {
attributes: [
{
'aria-hidden': 'true',
'data-test': 'test'
}
]
}
},
]
})
],
replaceOutputDir: output => {
return output.replace(/src\//, 'dist/')
}
});
// 再帰的にディレクトリ内のすべてのSVGファイルを取得
function getAllSvgFiles(dir) {
let results = [];
const list = fs.readdirSync(dir);
list.forEach(file => {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat && stat.isDirectory()) {
results = results.concat(getAllSvgFiles(filePath));
} else if (filePath.endsWith('.svg')) {
results.push(filePath);
}
});
return results;
}
const svgFiles = getAllSvgFiles('dist');
// 圧縮・最適化が完了した後にプレフィックス処理を実行
svgFiles.forEach(file => {
const svgContent = fs.readFileSync(file, 'utf-8');
const dom = new JSDOM(svgContent, { contentType: 'application/xml' });
const document = dom.window.document;
// ID名に一意のプレフィックスを追加
const elementsWithId = document.querySelectorAll('[id]');
elementsWithId.forEach((element) => {
const id = element.getAttribute('id');
if (id) {
const prefix = crypto.createHash('md5').update(file).digest('hex').slice(0, 8);
const newId = `${prefix}-${id}`;
element.setAttribute('id', newId);
// 参照している属性も更新
const references = document.querySelectorAll(`[href="#${id}"], [xlink\:href="#${id}"], [clip-path="url(#${id})"], [fill="url(#${id})"], [filter="url(#${id})"]`);
references.forEach(ref => {
const attr = [...ref.attributes].find(attr => attr.value === `url(#${id})`);
if (attr) {
const attrName = attr.name;
ref.setAttribute(attrName, `url(#${newId})`);
}
});
}
});
// class名に一意のプレフィックスを追加
const elementsWithClass = document.querySelectorAll('[class]');
elementsWithClass.forEach((element) => {
const classNames = element.getAttribute('class');
if (classNames) {
const prefix = crypto.createHash('md5').update(file).digest('hex').slice(0, 8);
const newClassNames = classNames.split(' ').map(className => `${prefix}-${className}`).join(' ');
element.setAttribute('class', newClassNames);
}
});
fs.writeFileSync(file, document.documentElement.outerHTML, 'utf-8');
});
})();
imagemin.js の解説
svgo プラグイン
-
removeDimensions
SVG をレスポンシブ対応させる。- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22" width="22" height="22"> + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
-
removeXMLProcInst
XML 宣言(<?xml ... ?>)を削除する。
-
removeComments
ソースコード中のコメントを削除する。
-
removeMetadata
<metadata> タグを削除する。
-
collapseGroups
不要なグループを解除する。<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22"> - <g> <path d="...省略..."/> <path d="...省略..."/> - </g>
-
convertStyleToAttrs
インラインのスタイルを対応する SVG 属性に変換する。- <path class="cls-1" d="...省略..." style="fill:#ccc"/> + <path class="cls-1" d="...省略..." fill="#ccc"/>
-
sortAttrs
各要素の属性をアルファベット順にソートする。
-
convertShapeToPath
<circle> タグなどの図形要素を <path> 要素に変換する。- <rect width="10" height="10"/> + <path d="...省略..."/>
-
convertColors
色表現を短い形式(例: #ffffff → #fff )に変換する。
-
convertPathData
<path> タグなどのパスデータを最適化する。- 座標をより短い指定方式(相対または絶対)に変換
- パスコマンドの変換
(例:直線のようなベジェ曲線を LineTo パスコマンドに変換) - 冗長なパスコマンドの削除
(例:現在の位置に移動するコマンドの削除) - 冗長な区切り文字・先頭のゼロの削除
- 丸め規則を用いた数値の調整
- <path d="M11 21C16.5228 21 21 16.5228 21 11C21 5.47715 16.5228 1 11 1C5.47715 1 1 5.47715 1 11C1 16.5228 5.47715 21 11 21ZM11 22C17.0751 22 22 17.0751 22 11C22 4.92487 17.0751 0 11 0C4.92487 0 0 4.92487 0 11C0 17.0751 4.92487 22 11 22Z"/> + <path d="M11 21c5.523 0 10-4.477 10-10S16.523 1 11 1 1 5.477 1 11s4.477 10 10 10m0 1c6.075 0 11-4.925 11-11S17.075 0 11 0 0 4.925 0 11s4.925 11 11 11"/>
-
removeAttrs
指定した属性を削除する。
-
inlineStyles
<style> タグ内のスタイルをインラインに変換する。- <defs> - <style> - .cls-1 { - fill: #ccc; - } - </style> - </defs> - <path class="cls-1" d="...省略..."/> + <path class="cls-1" d="...省略..." style="fill:#ccc"/>
-
cleanupIds
未使用のIDを削除・IDを短縮する。
-
addAttributesToSVGElement
<svg> タグに任意の属性を追加する。
ID・clsss にプレフィックス(接頭辞)を追加
異なる SVG 間で ID や class 名が競合すると、バグの原因となることがあります。
(例:異なるアイコンが同じスタイルを持つ、アニメーションが正しく動作しないなど)
これを避けるために、ファイルパスをもとにハッシュを生成して、プレフィックスとして追加してます。
SVG 最適化の Before / After
よく使用するグラフィックツールで SVG を書き出し、比較しました。
追加した属性もありますが、全体的に容量が抑えられています。
figma.svg
before(1,075 バイト)
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22 11C22 17.0751 17.0751 22 11 22C4.92487 22 0 17.0751 0 11C0 4.92487 4.92487 0 11 0C17.0751 0 22 4.92487 22 11Z" fill="#CCCCCC"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11 21C16.5228 21 21 16.5228 21 11C21 5.47715 16.5228 1 11 1C5.47715 1 1 5.47715 1 11C1 16.5228 5.47715 21 11 21ZM11 22C17.0751 22 22 17.0751 22 11C22 4.92487 17.0751 0 11 0C4.92487 0 0 4.92487 0 11C0 17.0751 4.92487 22 11 22Z" fill="black"/>
<g clip-path="url(#clip0_106_5)">
<path d="M9.43 12.62C9.41 11.2 9.67 10.67 10.85 9.66C11.62 9 11.83 8.68 11.83 8.14C11.83 7.6 11.54 7.29 10.96 7.29C10.29 7.29 9.97 7.67 9.62 8.92L7 8.49C7.14 7.59 7.5 6.78 8.01 6.17C8.65 5.4 9.68 5 11 5C13.4 5 14.65 6.14 14.65 8.3C14.65 9.53 14.27 10.16 12.7 11.41C11.91 12.03 11.64 12.43 11.64 12.99V13.28H9.43V12.62ZM9.19 14.17H11.9V16.63H9.19V14.17Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0_106_5">
<rect width="7.65" height="11.64" fill="white" transform="translate(7 5)"/>
</clipPath>
</defs>
</svg>
after(821 バイト)
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" data-test="test" viewBox="0 0 22 22"><path fill="#CCC" d="M22 11c0 6.075-4.925 11-11 11S0 17.075 0 11 4.925 0 11 0s11 4.925 11 11"/><path fill="#000" fill-rule="evenodd" d="M11 21c5.523 0 10-4.477 10-10S16.523 1 11 1 1 5.477 1 11s4.477 10 10 10m0 1c6.075 0 11-4.925 11-11S17.075 0 11 0 0 4.925 0 11s4.925 11 11 11" clip-rule="evenodd"/><g clip-path="url(#16c3fd09-a)"><path fill="#000" d="M9.43 12.62c-.02-1.42.24-1.95 1.42-2.96.77-.66.98-.98.98-1.52s-.29-.85-.87-.85c-.67 0-.99.38-1.34 1.63L7 8.49c.14-.9.5-1.71 1.01-2.32C8.65 5.4 9.68 5 11 5c2.4 0 3.65 1.14 3.65 3.3 0 1.23-.38 1.86-1.95 3.11-.79.62-1.06 1.02-1.06 1.58v.29H9.43zm-.24 1.55h2.71v2.46H9.19z"/></g><defs><clipPath id="16c3fd09-a"><path fill="#fff" d="M7 5h7.65v11.64H7z"/></clipPath></defs></svg>
illustrator.svg
before(748 バイト)
<?xml version="1.0" encoding="UTF-8"?><svg id="_レイヤー_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><defs><style>.cls-1{fill:#ccc;}</style></defs><g id="_レイヤー_1-2"><path class="cls-1" d="M10,19.5C4.76,19.5.5,15.24.5,10S4.76.5,10,.5s9.5,4.26,9.5,9.5-4.26,9.5-9.5,9.5Z"/><path d="M10,1c4.96,0,9,4.04,9,9s-4.04,9-9,9S1,14.96,1,10,5.04,1,10,1M10,0C4.48,0,0,4.48,0,10s4.48,10,10,10,10-4.48,10-10S15.52,0,10,0h0Z"/><path d="M8.61,11.8c-.02-1.42.24-1.95,1.42-2.96.77-.66.98-.98.98-1.52s-.29-.85-.87-.85c-.67,0-.99.38-1.34,1.63l-2.62-.43c.14-.9.5-1.71,1.01-2.32.64-.77,1.67-1.17,2.99-1.17,2.4,0,3.65,1.14,3.65,3.3,0,1.23-.38,1.86-1.95,3.11-.79.62-1.06,1.02-1.06,1.58v.29h-2.21v-.66ZM8.37,13.35h2.71v2.46h-2.71v-2.46Z"/></g></svg>
after(625 バイト)
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" data-test="test" viewBox="0 0 20 20"><path fill="#ccc" d="M10 19.5C4.76 19.5.5 15.24.5 10S4.76.5 10 .5s9.5 4.26 9.5 9.5-4.26 9.5-9.5 9.5"/><path d="M10 1c4.96 0 9 4.04 9 9s-4.04 9-9 9-9-4.04-9-9 4.04-9 9-9m0-1C4.48 0 0 4.48 0 10s4.48 10 10 10 10-4.48 10-10S15.52 0 10 0"/><path d="M8.61 11.8c-.02-1.42.24-1.95 1.42-2.96.77-.66.98-.98.98-1.52s-.29-.85-.87-.85c-.67 0-.99.38-1.34 1.63l-2.62-.43c.14-.9.5-1.71 1.01-2.32.64-.77 1.67-1.17 2.99-1.17 2.4 0 3.65 1.14 3.65 3.3 0 1.23-.38 1.86-1.95 3.11-.79.62-1.06 1.02-1.06 1.58v.29H8.61zm-.24 1.55h2.71v2.46H8.37z"/></svg>
xd.svg
before(679 バイト)
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><g transform="translate(-368 -165)"><circle cx="10" cy="10" r="10" transform="translate(368 165)" fill="#ccc"/><path d="M10,1a9,9,0,1,0,9,9,9.01,9.01,0,0,0-9-9m0-1A10,10,0,1,1,0,10,10,10,0,0,1,10,0Z" transform="translate(368 165)"/><path d="M3.408-3.36H5.616v-.288c0-.56.272-.96,1.056-1.584C8.24-6.48,8.624-7.1,8.624-8.336c0-2.16-1.248-3.3-3.648-3.3a3.719,3.719,0,0,0-2.992,1.168A4.8,4.8,0,0,0,.976-8.144L3.6-7.712c.352-1.248.672-1.632,1.344-1.632a.759.759,0,0,1,.864.848c0,.544-.208.864-.976,1.52a3.084,3.084,0,0,0-1.424,2.96ZM3.168,0h2.7V-2.464h-2.7Z" transform="translate(373 181)"/></g></svg>
after(639 バイト)
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" data-test="test" viewBox="0 0 20 20"><g transform="translate(-368 -165)"><circle cx="10" cy="10" r="10" fill="#ccc" transform="translate(368 165)"/><path d="M378 166a9 9 0 1 0 9 9 9.01 9.01 0 0 0-9-9m0-1a10 10 0 1 1-10 10 10 10 0 0 1 10-10"/><path d="M376.408 177.64h2.208v-.288c0-.56.272-.96 1.056-1.584 1.568-1.248 1.952-1.868 1.952-3.104 0-2.16-1.248-3.3-3.648-3.3a3.72 3.72 0 0 0-2.992 1.168 4.8 4.8 0 0 0-1.008 2.324l2.624.432c.352-1.248.672-1.632 1.344-1.632a.76.76 0 0 1 .864.848c0 .544-.208.864-.976 1.52a3.08 3.08 0 0 0-1.424 2.96Zm-.24 3.36h2.7v-2.464h-2.7Z"/></g></svg>