Обработка SVG: как ресурс, как компонент, как спрайт

SVG-файлы в Webpack могут обрабатываться несколькими способами. Один и тот же файл способен выступать:

  • обычным статическим ресурсом;
  • встроенным Data URI;
  • React-компонентом;
  • элементом SVG-спрайта;
  • строковым содержимым;
  • инлайн-разметкой внутри HTML.

Способ обработки зависит от архитектуры проекта, требований к производительности и используемого UI-фреймворка.

Webpack 5 значительно упростил работу с SVG благодаря Asset Modules, однако SVG остаётся особым типом ресурса, поскольку сочетает свойства изображения и XML-документа.


SVG как обычный ресурс

Наиболее простой вариант — обработка SVG как файла, который копируется в итоговую сборку.

Конфигурация через asset/resource

module.exports = {
    module: {
        rules: [
            {
                test: /\.svg$/i,
                type: 'asset/resource'
            }
        ]
    }
};

Импорт SVG

import logo from './images/logo.svg';

console.log(logo);

После сборки импорт будет содержать URL:

/assets/logo.a1b2c3.svg

Использование в DOM

const image = document.createElement('img');

image.src = logo;

document.body.append(image);

Что делает asset/resource

Webpack:

  1. копирует SVG в output-директорию;
  2. генерирует уникальное имя;
  3. возвращает публичный URL;
  4. учитывает hashing для кеширования.

Настройка имени SVG-файлов

Генерация имён

module.exports = {
    module: {
        rules: [
            {
                test: /\.svg$/i,
                type: 'asset/resource',
                generator: {
                    filename: 'images/[name].[contenthash][ext]'
                }
            }
        ]
    }
};

Результат

dist/
└── images/
    └── logo.6fd9e1.svg

Популярные шаблоны

Шаблон Назначение
[name] имя файла
[ext] расширение
[hash] общий hash
[contenthash] hash содержимого
[path] исходный путь

SVG как встроенный Data URI

SVG может внедряться прямо в JavaScript или CSS.

Конфигурация

module.exports = {
    module: {
        rules: [
            {
                test: /\.svg$/i,
                type: 'asset/inline'
            }
        ]
    }
};

Импорт

import icon from './icon.svg';

console.log(icon);

Результат:

data:image/svg+xml;base64,...

Когда inline SVG полезен

Data URI подходит для:

  • маленьких иконок;
  • UI-символов;
  • чекбоксов;
  • стрелок;
  • служебной графики.

Преимущества

  • отсутствие дополнительного HTTP-запроса;
  • быстрый рендер;
  • уменьшение количества файлов.

Недостатки

  • увеличение размера JS-бандла;
  • отсутствие независимого кеширования;
  • ухудшение читаемости sourcemap.

Автоматический выбор между inline и resource

Webpack позволяет автоматически определять режим обработки по размеру файла.

Использование asset

module.exports = {
    module: {
        rules: [
            {
                test: /\.svg$/i,
                type: 'asset',
                parser: {
                    dataUrlCondition: {
                        maxSize: 4 * 1024
                    }
                }
            }
        ]
    }
};

Как это работает

  • SVG меньше 4 KB → inline;
  • SVG больше 4 KB → отдельный файл.

Это оптимальный компромисс для большинства проектов.


SVG внутри CSS

Импорт через url()

.logo {
    background-image: url('./logo.svg');
}

Конфигурация

module.exports = {
    module: {
        rules: [
            {
                test: /\.css$/i,
                use: ['style-loader', 'css-loader']
            },
            {
                test: /\.svg$/i,
                type: 'asset/resource'
            }
        ]
    }
};

Webpack:

  1. обнаруживает SVG;
  2. добавляет его в граф зависимостей;
  3. генерирует URL;
  4. подставляет путь в CSS.

SVG как исходный текст

Иногда требуется получить XML-содержимое файла.

Использование asset/source

module.exports = {
    module: {
        rules: [
            {
                test: /\.svg$/i,
                type: 'asset/source'
            }
        ]
    }
};

Импорт

import svgContent from './icon.svg';

console.log(svgContent);

Результат:

<svg xmlns="http://www.w3.org/2000/svg">

Когда нужен source

Подход полезен при:

  • динамической модификации SVG;
  • генерации иконок;
  • вставке через innerHTML;
  • SSR;
  • работе с SVG-парсерами.

SVG как React-компонент

Один из наиболее популярных сценариев — преобразование SVG в React-компонент.

Для этого обычно используется SVGR.


Установка SVGR

npm install @svgr/webpack --save-dev

Базовая конфигурация

module.exports = {
    module: {
        rules: [
            {
                test: /\.svg$/i,
                issuer: /\.[jt]sx?$/,
                use: ['@svgr/webpack']
            }
        ]
    }
};

Импорт SVG как компонента

import Logo from './logo.svg';

export default function App() {
    return <Logo />;
}

SVG превращается в полноценный React-компонент.


Что генерирует SVGR

Исходный SVG:

<svg width="100" height="100">
    <circle cx="50" cy="50" r="40" />
</svg>

Преобразуется примерно в:

const SvgComponent = (props) => (
    <svg width="100" height="100" {...props}>
        <circle cx="50" cy="50" r="40" />
    </svg>
);

export default SvgComponent;

Преимущества SVG-компонентов

Управление через props

<Logo width={200} height={200} />

Стилизация

<Logo className="icon" />

Динамические цвета

<Logo fill="red" />

Интеграция с React

SVG становится частью компонентной системы.


currentColor и SVG

Наиболее удобная практика — использование currentColor.

SVG

<svg fill="currentColor">

React

<Logo style={{ color: 'tomato' }} />

SVG автоматически наследует цвет текста.


Одновременная поддержка URL и React-компонентов

Часто требуется:

  • иногда импортировать SVG как компонент;
  • иногда получать URL.

Конфигурация resourceQuery

module.exports = {
    module: {
        rules: [
            {
                test: /\.svg$/i,
                oneOf: [
                    {
                        resourceQuery: /url/,
                        type: 'asset/resource'
                    },
                    {
                        issuer: /\.[jt]sx?$/,
                        use: ['@svgr/webpack']
                    }
                ]
            }
        ]
    }
};

Использование

Как компонент

import Logo from './logo.svg';

Как URL

import logoUrl from './logo.svg?url';

Оптимизация SVG через SVGO

SVGR может автоматически оптимизировать SVG.

Установка

npm install svgo --save-dev

Конфигурация

{
    test: /\.svg$/i,
    use: [
        {
            loader: '@svgr/webpack',
            options: {
                svgo: true
            }
        }
    ]
}

Что делает SVGO

SVGO:

  • удаляет комментарии;
  • сокращает координаты;
  • убирает лишние атрибуты;
  • минимизирует XML;
  • объединяет path.

Опасность агрессивной оптимизации

Некоторые SVG могут ломаться после оптимизации.

Особенно часто проблемы возникают с:

  • viewBox;
  • gradients;
  • masks;
  • filters;
  • animate;
  • clipPath.

Сохранение viewBox

Очень важная настройка:

options: {
    svgoConfig: {
        plugins: [
            {
                name: 'preset-default',
                params: {
                    overrides: {
                        removeViewBox: false
                    }
                }
            }
        ]
    }
}

Без viewBox SVG теряет адаптивность.


SVG-спрайты

SVG-спрайт — единый SVG-файл, содержащий множество иконок.

Преимущества

  • один HTTP-запрос;
  • хорошее кеширование;
  • компактность;
  • удобная система иконок.

Принцип работы спрайта

Спрайт содержит набор символов:

<symbol id="search">
    <path d="..." />
</symbol>

<symbol id="close">
    <path d="..." />
</symbol>

Использование:

<svg>
    <use href="#search"></use>
</svg>

svg-sprite-loader

Наиболее популярный инструмент — svg-sprite-loader.

Установка

npm install svg-sprite-loader --save-dev

Конфигурация

module.exports = {
    module: {
        rules: [
            {
                test: /\.svg$/,
                include: /icons/,
                use: [
                    {
                        loader: 'svg-sprite-loader',
                        options: {
                            symbolId: '[name]'
                        }
                    }
                ]
            }
        ]
    }
};

Импорт иконок

import './icons/search.svg';
import './icons/close.svg';

Использование иконок

<svg class="icon">
    <use href="#search"></use>
</svg>

symbolId

Настройка определяет идентификатор иконки.

Пример

symbolId: 'icon-[name]'

Результат:

<use href="#icon-search"></use>

Разделение SVG по назначению

На практике SVG обычно делят на категории.

Иконки

src/icons/

Используются как спрайт.

Иллюстрации

src/images/

Используются как asset/resource.

React SVG

src/components/icons/

Используются через SVGR.


Комбинирование loader-цепочек

SVG может проходить через несколько loader одновременно.

Пример

{
    test: /\.svg$/i,
    use: [
        '@svgr/webpack',
        'url-loader'
    ]
}

Порядок выполнения loader

Webpack выполняет loaders справа налево.

Последовательность

url-loader → @svgr/webpack

Это критически важно при сложной обработке SVG.


issuer и разделение логики

Часто SVG должны обрабатываться по-разному в зависимости от источника импорта.

Пример

{
    test: /\.svg$/i,
    issuer: /\.css$/,
    type: 'asset/resource'
}

Для React

{
    test: /\.svg$/i,
    issuer: /\.[jt]sx?$/,
    use: ['@svgr/webpack']
}

oneOf для оптимизации правил

Вместо множества независимых rules лучше использовать oneOf.

Пример

{
    test: /\.svg$/i,
    oneOf: [
        {
            resourceQuery: /component/,
            use: ['@svgr/webpack']
        },
        {
            resourceQuery: /inline/,
            type: 'asset/inline'
        },
        {
            type: 'asset/resource'
        }
    ]
}

Использование resourceQuery

React-компонент

import Icon from './icon.svg?component';

Inline SVG

import dataUrl from './icon.svg?inline';

Обычный файл

import iconUrl from './icon.svg';

Производительность SVG

SVG как компонент

Подходит для:

  • иконок;
  • UI;
  • интерактивной графики.

SVG как resource

Подходит для:

  • крупных иллюстраций;
  • баннеров;
  • схем;
  • инфографики.

SVG-спрайт

Подходит для:

  • системных иконок;
  • дизайн-систем;
  • UI-kit.

Проблемы безопасности

SVG является XML-документом и способен содержать:

  • JavaScript;
  • внешние ссылки;
  • embedded-объекты;
  • вредоносные payload.

Особенно опасны

<script>
    alert(1)
</script>

Sanitization SVG

При загрузке SVG от пользователей необходимо:

  • очищать SVG;
  • удалять script;
  • удалять foreignObject;
  • фильтровать атрибуты;
  • использовать sanitizers.

SVG и tree shaking

SVG-компоненты могут участвовать в tree shaking.

Пример

import SearchIcon from './search.svg';

Неиспользуемые SVG не попадут в bundle при корректной конфигурации.


Lazy loading SVG

Крупные SVG можно загружать динамически.

Пример

const illustration = await import('./hero.svg');

SVG и code splitting

Webpack способен выносить SVG в отдельные chunks.

Особенно полезно для:

  • тяжёлых иллюстраций;
  • карт;
  • диаграмм;
  • лениво загружаемых модулей.

Source maps и SVG

SVG обычно не требуют sourcemap, однако при inline SVG внутри JS размер карт может заметно увеличиваться.

Особенно это касается:

  • больших SVG;
  • Data URI;
  • встроенных base64.

Наиболее распространённая архитектура

Для React-проекта

icons/        → sprite или svgr
images/       → asset/resource
ui-icons/     → svgr
illustrations → resource

Типичная конфигурация

module.exports = {
    module: {
        rules: [
            {
                test: /\.svg$/i,
                oneOf: [
                    {
                        include: /icons/,
                        use: ['@svgr/webpack']
                    },
                    {
                        type: 'asset/resource'
                    }
                ]
            }
        ]
    }
};

Основные подходы к обработке SVG

Подход Назначение
asset/resource отдельный файл
asset/inline Data URI
asset/source строковое содержимое
SVGR React-компонент
svg-sprite-loader SVG-спрайт
asset автоматический режим

Ключевые различия подходов

Способ HTTP-запрос Управление стилями Кеширование
resource да ограничено хорошее
inline нет ограничено отсутствует
component нет максимальное внутри JS
sprite один запрос хорошее отличное