SVGの実運用: パフォーマンスと安全性 2025 — 最小限で最大効果

公開: 2025年9月23日 · 読了目安: 5 · 著者: Unified Image Tools 編集部

TL;DR

  • パス/トランスフォームの正規化、テキストは必要ならばアウトライン化
  • 不要なメタ/エディタ痕跡は削除、viewBoxと比率でCLS回避
  • 外部リソース参照/スクリプトは制限—CSPと併せて管理

内部リンク: アクセシビリティ実務の画像 — alt/装飾/図解の線引き 2025, OGPサムネイル設計 2025 — 見切れない、重くない、伝わる

はじめに

SVG は「テキストで記述されたベクター画像」ゆえに、軽量・拡大縮小で劣化しない・スタイル適用が容易という利点があります。一方で、編集ソフト由来の冗長ノードやスクリプト/外部参照によるリスク、重いフィルターの使用でパフォーマンスを損ねる落とし穴もあります。本稿は、実運用での最小コスト最大効果をめざし、最適化・安全性・配信・アクセシビリティを横断的に整理します。

パフォーマンス最適化(構造と座標のダイエット)

  • SVGO 等での自動最適化を既定化(CI で強制)
    • precision の設定(例: 2〜3桁)で座標を丸め、convertPathData/convertTransform/mergePaths を有効化
    • removeMetadata/removeEditorsNSData/cleanupIDs などでエディタ痕跡を除去
  • viewBox を基準にし、width/height は用途ごとに CSS で調整(レスポンシブ対応)
  • preserveAspectRatio を明示(例: xMidYMid meet)して歪み/CLS を防止
  • パスにまとめすぎず、再利用可能な要素は <symbol> 化してスプライト配信
  • 重いフィルター/ブラー/多段グラデーションは避け、ビットマップ代替や CSS での近似へ切り替え

SVGO 設定例(svgo.config.js):

module.exports = {
  multipass: true,
  js2svg: { indent: 0, pretty: false },
  plugins: [
    { name: 'preset-default', params: { overrides: { removeViewBox: false } } },
    'cleanupIds',
    'convertPathData',
    'convertTransform',
    { name: 'removeAttrs', params: { attrs: ['data-*', 'id'] } },
  ],
};

スプライト戦略(<symbol> + <use>)

多アイコンでの HTTP 負荷と再描画コストを抑えるには、ビルド時に 1 ファイルへ統合しキャッシュを効かせます。

<svg xmlns="http://www.w3.org/2000/svg" style="display:none">
  <symbol id="icon-search" viewBox="0 0 24 24">...</symbol>
  <symbol id="icon-close" viewBox="0 0 24 24">...</symbol>
</svg>

<!-- 使い方 -->
<svg class="icon" aria-hidden="true"><use href="#icon-search" /></svg>

注意: 外部スプライト参照(href="/sprite.svg#icon")は CORS や CSP と相性があるため、同一オリジン/インラインを基本にします。

セキュリティとサニタイズ(XSS/外部参照の封じ込め)

SVG にはスクリプト/イベントハンドラ/foreignObject など動的機能があり、入力データとして扱う場合は XSS の温床になり得ます。以下を原則化します。

  • script/foreignObject/on* 属性は拒否(ビルドとサニタイザで二重に防ぐ)
  • 外部参照(<use href>, <image href>, <link>)を禁止/同一オリジン限定
  • データ URL での javascript: 等をブロック
  • MIME を image/svg+xml に固定し、任意のコンテンツ挿入を防ぐ

DOMPurify 例(Node/Edge):

import createDOMPurify from 'dompurify';
import { JSDOM } from 'jsdom';

const window = new JSDOM('').window;
const DOMPurify = createDOMPurify(window);

export function sanitizeSVG(svg) {
  return DOMPurify.sanitize(svg, {
    USE_PROFILES: { svg: true },
    FORBID_TAGS: ['script', 'foreignObject'],
    FORBID_ATTR: ['on*', 'style'], // inline style を避ける運用なら
    ALLOWED_URI_REGEXP: /^(data:image\/(svg\+xml|png|jpeg);|https?:|#)/i,
  });
}

CSP(HTTP ヘッダーまたは <meta httpEquiv>)例:

Content-Security-Policy:
  default-src 'self';
  img-src 'self' data: https:;
  object-src 'none';
  script-src 'self';
  style-src 'self' 'unsafe-inline';

配信とキャッシュ(HTTP レイヤの最適化)

  • Content-Type は image/svg+xml; charset=utf-8
  • テキスト圧縮(Brotli/gzip)を有効化。拡張子 .svgz は運用コストが高いため通常は不要
  • バージョン付きファイル名で Cache-Control: max-age=31536000, immutable
  • インライン <svg> は初期表示には速いが、再利用性/キャッシュが効かない点に留意(小規模アイコンのみ)
  • 外部参照は同一オリジンで配信し、CORS を不要に。SRI は同一オリジンでは原則不要

Next.js でのインライン SVG(アクセシブルなラッパー):

type Props = { title?: string; desc?: string; focusable?: boolean } & React.SVGProps<SVGSVGElement>;

export function Icon(props: Props) {
  const { title, desc, focusable = false, ...rest } = props;
  const titleId = title ? 'svg-title' : undefined;
  const descId = desc ? 'svg-desc' : undefined;
  return (
    <svg role="img" aria-labelledby={[titleId, descId].filter(Boolean).join(' ') || undefined} focusable={focusable} {...rest}>
      {title && <title id={titleId}>{title}</title>}
      {desc && <desc id={descId}>{desc}</desc>}
      {/* ...パス... */}
    </svg>
  );
}

アクセシビリティ(意味付けとフォーカス管理)

  • 意味のある図像には <title>/<desc> を与え role="img"、装飾には aria-hidden="true"/focusable="false"
  • テキストをパス化する場合は可読性・拡大時の輪郭を確認(ヒンティング代替)
  • アニメーションは prefers-reduced-motion に準拠し、ユーザー選好を尊重

フォールバックと代替案

  • 旧環境では PNG/WebP の代替を用意(ビットマップ化はビルドで自動生成)
  • <picture> で用途により切り替えるか、重要でない装飾は noscript でフェイルセーフ
<picture>
  <source type="image/svg+xml" srcset="/logo.svg" />
  <img src="/logo.png" width="200" height="40" alt="サイトロゴ" />
  <noscript><img src="/logo.png" alt="サイトロゴ" /></noscript>
  
</picture>

ケーススタディ(短編)

事例1: エディタ出力のまま配信して描画が重い

  • 症状: 余剰グループ/座標精度が過剰、フィルターが多用され初回描画が遅い
  • 対策: SVGO で precision=3、convertPathData/mergePaths、フィルター廃止
  • 結果: ファイル 42% 減、TTI 改善、再描画時の CPU 使用率も低下

事例2: 外部スプライト参照で CSP によるブロック

  • 症状: <use href="/sprite.svg#icon"> が CSP でブロックされアイコン欠落
  • 対策: インラインスプライトへ変更/同一オリジン配信、CSP を最小緩和
  • 結果: 表示安定、キャッシュ効率も改善

FAQ

Q. 文字はアウトライン化すべき?

A. ロゴなど形状維持が重要な場合はパス化が安全。本文テキストはアウトライン化しない(可読性/翻訳性を損なう)。

Q. <image> でビットマップを埋め込むのは?

A. 可能ですが、外部参照やデータ URL の扱いに注意。色空間/サイズの管理が難しくなるため、用途は限定的に。

Q. フィルター表現はどうする?

A. まず CSS で代替し、どうしても必要なら解像度を落としたラスタライズ版の併用を検討。

チェックリスト(配信用)

  • [ ] SVGO を CI で強制(precision/convertPathData/cleanupIds)
  • [ ] viewBox/preserveAspectRatio を明示、CLS ゼロ
  • [ ] 重いフィルター/ブラーを排除(必要ならラスタ代替)
  • [ ] script/foreignObject/on* を禁止し、サニタイズ + CSP を適用
  • [ ] 同一オリジンで配信、image/svg+xml + 圧縮 + 長期キャッシュ
  • [ ] アクセシビリティ(title/desc/role/focusable)とフォールバックを用意

関連記事