Tech

Vite×Vue系環境でSVG画像を扱うならほぼ必須な設定〜SVGO圧縮に気をつけろ〜

最終更新 公開

前提知識

Viteの環境設定は vite.config.js (TypeScript環境なら .ts )というファイルで行います。

https://ja.vite.dev/config

コマンドラインから vite を実行すると、Vite はプロジェクトルート内の vite.config.js という名前の設定ファイルを自動的に解決しようとします(その他の JS および TS の拡張子もサポートされています)。

この設定ファイルはフレームワークを連携した場合を除き※プロジェクト立ち上げの初期時点では用意されていないことがあります。これはnpm-scriptsなど様々なツールを組み合わせて環境構築をしていく場合と違い、Viteはデフォルトで必要なものは揃っている状態からなお「+α」でカスタマイズしたい場合にのみconfigを作成(=オプトイン)する、という設計思想によるもののようです。

※ただし、今回の前提であるVue系環境と連携した場合はあらかじめ vite.config.js/.ts が作成(=専用のプラグイン @vitejs/plugin-vue が導入)されるので、「無いときもある」ぐらいとして捉えてください。なお、Nuxtと連携をした場合は nuxt.config.ts が代替ファイルになります。

問題

特に環境構築時点で深く考えずスピーディーに使えることがViteの利点でもあるのですが、近年のWebサイト制作で扱うことの多いSVG画像をデフォルト環境上で扱いたい場合、正直なところ困りました。

Vite側はSVGを他の画像形式と同じく「静的アセット」として扱うため、(元々サイズが大きくなりすぎない形式とはいえ)圧縮などの最適化処理は特に行ってくれません。特にVueのようなJSフレームワーク上で画像を扱いたい場合では、import imgUrl from './img.svg のようにインポート構文で書くことが多いのですが……実際に読み出す時に返ってくるのはURL文字列で、このままだとInline SVG( <svg>〜</svg> )として扱うことはできないため、外部からの干渉=CSSによるSVG内部カラーの書き換えなどを行うこともできません。

https://ja.vite.dev/guide/assets

静的アセットをインポートすると、配信された際に解決されたパブリックな URL が返されます
例えば、 imgUrl は、開発中は /src/img.png となり、本番用ビルドでは /assets/img.2d8efhg.png となります。

(通常の画像ならほとんど気にならないこととはいえ)このように開発用と本番用で出力URLが異なってしまう件も、そもそもInline SVGで返すことができれば意識することはないはずです。

余談:デフォルトでInline SVGを実現する方法(と注意点)

アセットは末尾に ?raw を付与することで文字列としてインポートすることができます。

この通りimport時の末尾に ?raw を付けることで 「文字列」としての出力は可能 なので、アセットURLではなく <svg>〜</svg> の出力として返すことは可能です。ただし実際に読み込む際には v-html を使う必要があったり、ラッパー要素が残ってしまったりといった引っかかりポイントがあります。

<!-- Vue -->
<script setup>
import imgRaw from './img.svg?raw
</script>

<template>
  <!-- 実際の出力(ラッパーとしてのspanは残る) -->
  <span v-html="imgRaw" />
</template>
<!-- 実際の出力(ラッパーとしてのspanは残る) -->
<span><svg>〜</svg></span>

あくまで ?raw による出力は「文字列」のため、ラッパーである <span> の中身にHTMLを流し込むこと( innerHTML に近い挙動)になります。ただ、実際にはVueコンポーネントとして扱えているわけではないですし、「HTMLを流し込む」=XSS攻撃などの温床になることもあり得ます。SVGをそのまま埋め込みたいだけなのに……。

※またViteには ?inline / ?no-inline というサフィックスも用意されていますが、これはbase64による画像→文字列変換を行うかどうかを明示するためのパラメータで、Inline SVGの用途とは関係ありません。ただしこれらを付けていないと(そもそもSVG形式に限らず)Vite上の軽量な静的アセットはURLではなくbase64変換後の文字列が埋め込まれた状態で出力されます。

assetsInlineLimit オプション で指定したバイト数よりも小さいアセットは base64 データの URL としてインライン化されます

これを回避するためにはimport時に ?no-inline を使うか、上記の assetsInlineLimit のデフォルト値 4096 (4 KiB) をconfigで書き換えるかになるので、混同しないよう注意してください。

設定

VueコンポーネントとしてSVG画像を取り扱いたい場合、冒頭で振れたconfigファイルと、以下 vite-svg-loader のようなプラグインの導入がほぼほぼ必須となります。

https://www.npmjs.com/package/vite-svg-loader

npm install --save-dev vite-svg-loader (または yarn add vite-svg-loader --dev )した状態で、以下 vite.config.js をルートディレクトリに(なければ※)作成してください。

※ Vite × Vue3 の前提であれば、 vue() の次に svgLoader({...}) を追記するイメージです。

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import svgLoader from 'vite-svg-loader'

export default defineConfig({
  plugins: [
    vue(),
    svgLoader({
      defaultImport: 'url',  // デフォルトをURLに戻す(コンポーネントを主にする場合は不要)
      svgoConfig: {
        plugins: [
          {
            name: 'preset-default',
            params: {
              overrides: {
              // width/height があると viewBox を削除する既定挙動を無効化
                removeViewBox: false,
              },
            },
          },
        ],
      },
    }),
  ],
})

nuxt.config.jsの場合

  • defineConfigdefineNuxtConfig
  • Vite用のプラグインに関する記載が vite:plugins: のような入れ子
  • vue() ( @vitejs/plugin-vue )のimport・記載は不要
    (オプションが必要なら上と同じく vite:vue: のような入れ子)

になるなどの差分があります。

import svgLoader from 'vite-svg-loader'

export default defineNuxtConfig({
  vite: {
    plugins: [
      svgLoader({
        defaultImport: 'url',  // デフォルトをURLに戻す
        svgoConfig: {
          plugins: [
            { name: 'preset-default', params: { overrides: { removeViewBox: false } } },
          ],
        }
      })
    ],
  }
})

実用例

このプラグインの導入とconfig設定を行うことで、Viteのデフォルト(URL出力)を保ったまま、 ?component のサフィックスを付けてimportしたSVG画像をInline SVG出力することができます。

<!-- Vue -->
<script setup>
import imgUrl from './img.svg' // ?url をデフォルト(明示は不要)
import imgComponent from './img.svg?component' // コンポーネント化(明示が必要)
</script>

<template>
  <!-- URL(デフォルト)出力 -->
  <img :src="imgUrl" alt="">
  <!-- Inline SVG(コンポーネント)出力 -->
  <component :is="imgComponent">
</template>
<!-- URL(デフォルト)出力 -->
<img src="/assets/img.[ハッシュ値].svg" alt="">
<!-- Inline SVG(コンポーネント)出力 -->
<svg>〜</svg>

設定の解説

defaultImport: 'url' は本来SVG全てをコンポーネント化する前提であれば不要な設定で、こちらの指定がなければ ?component のサフィックスを省略できます。(デフォルトでは逆にURL出力用には ?url を明示する必要がある)

ただ、ここまで話をしつつもSVG画像すべてをInline SVGにしたいか?というとそうでもない(色変更の必要のないアイコン埋め込みなどの)場面も多いので、デフォルトの挙動はVite標準に寄せておくようURL出力・Inline SVG化したい時だけ ?component を付ける状態にしています。
実際にVueテンプレート上でもコンポーネント化したSVGは <component :is="[import名]"> で読み込む形になるので、運用上もimportと単位が揃って分かりやすいので……(個人の感想です)。

また続く svgoConfig{ overrides: { removeViewBox: false } } についてはWebブラウザ上でSVGを扱う以上はほぼ必須の設定になります。

導入した vite-svg-loader には SVGO という圧縮ツールが内包されており、Vite単体では解決できないSVGファイルの圧縮・最適化処理を行ってくれて便利なのですが、この removeViewBox: false; を記載していない場合、処理後のSVGから viewBox というパラメータが削除されてしまいます。

https://svgo.dev/docs/plugins/removeViewBox

viewBoxがない状態のSVGはざっくり言うと画像表示サイズの拡大・縮小ができなくなります。

つまり元々SVG画像が持っている width height の指定サイズ通りには表示できますが、外部CSSによるサイズ変更を全く受け付けなくなってしまうので、Inline SVGどころかベクター画像としてSVG画像を埋め込むメリット自体がまるっとなくなってしまいます。なんなんだこれは。

なお、SVGOの名誉のために言っておくとこの圧縮時にViewBoxを外してしまう処理がデフォルトなのはv3まで、v4からはプリセット設定 preset-default から除外されているようです。

ただし2026年7月の現時点でも vite-svg-loader 自体はSVGOv3(v3.3.4)に依存しているようなので、今回のようにプリセットのオーバーライド設定がほぼ必須になるというオチでした……。

https://svgo.dev/docs/migrations/migration-from-v3-to-v4

removeViewBox is no longer a default plugin to preserve scalability.

このviewBox消す必要ある?議論自体は2019年頃から議論が白熱していたようなので、以下のIssuesを参考に見てみると面白いかもしれません👎

https://github.com/svg/svgo/issues/1128

いずれにせよ、現代においてSVG画像を扱う場合は width height そして viewBox の3種が揃った状態が最もWebブラウザ上での挙動が安定することは、覚えておいて損はないと思います。