1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

コーディングもするデザイナーが知っておきたい SVG 最適化(svgo v3 対応)

Last updated at Posted at 2024-10-14

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>

Github

ソースコード(GitHub)

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?