Погоня за чёткостью на плотных экранах больно бьёт по растровым картинкам: иконке нужен файл в трёх плотностях, логотипу своя версия под каждый брейкпоинт, и медианная страница в октябре 2024 года весила 2 652 KB на десктопе и 2 311 KB на мобильных, причём изображения занимали в этом объёме 1 054 KB, больше, чем скрипты, шрифты и стили вместе взятые. Векторная графика ломает эту арифметику: один SVG-файл одинаково хорош и на часах, и на большом мониторе, потому что рисуется командами, а не пикселями. Но у формата есть своя цена: экспорт из редакторов тянет за собой мусор, из-за которого двухкилобайтная иконка распухает до десятков килобайт. Путь от грязного исходника до лёгкого адаптивного файла проходит через чистку, правильный viewBox, спрайты и грамотную отдачу с сервера, и этот путь собран ниже по шагам.
Откуда в векторных файлах берётся лишний вес и мусор редакторов
Векторные редакторы сохраняют файлы для собственного удобства, а не для скорости сайта. Inkscape дописывает атрибуты с префиксами inkscape: и sodipodi: ко всем слоям, Illustrator оставляет комментарий генератора и метаданные документа, Sketch и Figma прячут в разметку служебные слои и скрытые фигуры. Типичный свежий экспорт выглядит примерно так:
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 120 120">
<title>icon-calendar</title>
<metadata>VectorEditor 3.2, layer tree, document settings</metadata>
<defs>
<linearGradient id="paint0">
<stop stop-color="#0a7f5c"/>
</linearGradient>
</defs>
<g inkscape:label="calendar" fill="rgb(0, 0, 0)" stroke="none">
<rect x="10.2000001" y="10.2000001" width="99.5999985"
height="99.5999985" rx="8"/>
</g>
</svg>
Здесь лишнего больше, чем кажется на первый взгляд. Метаданные и подпись слоя нужны редактору, а не браузеру; градиент paint0 никто не использует, потому что фигура залита чёрным; stroke со значением none совпадает со значением по умолчанию; запись цвета rgb(0, 0, 0) занимает одиннадцать символов там, где хватило бы краткой формы. Отдельная боль в числах: координата 10.2000001 хранит семь знаков после запятой, хотя при ширине холста 120 условных единиц уже третий знак меняет положение линии на одну тысячную ширины картинки. Сюда же ложатся DOCTYPE, инструкции обработки XML и комментарии, которые редакторы ставят в начало файла. Итог предсказуем: иллюстрация, которая в чистом виде весила бы два килобайта, распухает в десять раз.
Чистка SVGO и тонкая настройка плагинов под адаптивный проект
Стандартный инструмент чистки называется SVGO, SVG Optimizer: библиотека Node.js и консольная утилита в одном лице. Актуальная версия 4.1.0, за неделю из реестра npm её скачивают около 22 миллионов раз, и по этому показателю она входит в число самых востребованных инструментов фронтенда. Идея проста: исходники из редакторов несут избыточное, метаданные, комментарии, скрытые элементы, значения по умолчанию, и всё это убирается без малейшего изменения отрисовки.
Консольная работа выглядит так:
svgo icon.svg -o icon.min.svg
svgo -rf path/to/directory_with_svgs -o path/to/output_directory
svgo --config svgo.config.mjs -rf assets/icons -o dist/icons
Настройки живут в файле svgo.config.mjs:
export default {
multipass: true,
datauri: 'base64',
plugins: [
'prefixIds',
{
name: 'preset-default',
params: {
overrides: {
cleanupIds: false
}
}
}
]
};
Режим multipass гоняет цепочку плагинов по кругу, пока размер файла не перестаёт уменьшаться: удаление одной группы часто открывает дорогу другому плагину. Поле datauri готовит файл к встраиванию в CSS в виде data-URL. Отключение cleanupIds не каприз: при сборке спрайта идентификаторы градиентов и масок обязаны выжить, потому что на них ссылаются элементы use, а плагин prefixIds добавляет к именам префикс файла, чтобы градиенты разных иконок не конфликтовали в общем спрайте.
Набор preset-default в четвёртой версии запускает больше тридцати плагинов в строгом порядке. Шапку документа разбирают removeDoctype и removeXMLProcInst, мусор вычищают removeComments, removeMetadata и removeEditorsNSData, скрытые и пустые элементы убирают removeHiddenElems и removeEmptyText, примитивы превращает в компактные пути convertShapeToPath, команды сокращает convertPathData, координаты округляет cleanupNumericValues, вложенность схлопывают collapseGroups и mergePaths, а sortAttrs выравнивает атрибуты ради лучшего сжатия. Параметр floatPrecision у cleanupNumericValues задаёт число знаков после запятой, по умолчанию 3, и повышать его смысла нет.
Отдельная история с viewBox. В третьей версии SVGO плагин removeViewBox входил в набор по умолчанию и срезал атрибут viewBox у всех файлов, где проставлены width и height: классическая ловушка, из-за которой вычищенные иконки переставали растягиваться. В четвёртой версии плагин из набора по умолчанию убран, но наследие старых конфигов живо, и после чистки стоит открыть файл и убедиться, что viewBox на месте. Для сборочных скриптов у SVGO есть программный интерфейс optimize, а плагин vite-plugin-svgo 3.0.2 встраивает чистку прямо в конвейер Vite.
Механика viewBox и правила растягивания картинки на любом экране
Атрибут viewBox - сердце адаптивного вектора. Запись viewBox="0 0 600 400" описывает окно в систему координат: область от нулевой точки шириной 600 и высотой 400 условных единиц. Когда контейнер меняет размер, браузер пересчитывает масштаб так, чтобы содержимое окна вписалось, а preserveAspectRatio со значением xMidYMid meet, установленным по умолчанию, держит пропорции: картинка не обрезается и не сплющивается, лишнее место остаётся пустым.
Жёсткие width="120" height="120" на корневом элементе перебивают адаптивность: 120 пикселей остаются 120 пикселями и на телефоне, и на широком мониторе. Поэтому размер выносят в стили:
.frame {
width: 100%;
height: auto;
display: block;
}
<img src="/diagram.svg" alt="Схема сборки модуля" class="frame">
Значение block у display убирает щель под строчной картинкой, а height: auto вместе с viewBox позволяет браузеру вычислить пропорции без единой лишней строки: соотношение сторон берётся из самого файла. Дальше работает экономика формата. Растровой картинке для чёткости нужны версии в двойной и тройной плотности, атрибуты srcset и sizes в разметке и три файла на каждую иконку, вектору достаточно одного: линия масштабируется математически и остаётся резкой на самом плотном экране.
Спрайты и повторное использование без дублей и лишних запросов
Отдельный файл на каждую иконку означает отдельный сетевой запрос на каждую иконку. Спрайт решает проблему: все значки собираются в один файл и используются через элемент use со ссылками на идентификаторы.
<svg width="0" height="0" style="position:absolute">
<symbol id="icon-cart" viewBox="0 0 24 24">
<path d="M3 4h2l2.4 12.4a2 2 0 0 0 2 1.6h9a2 2 0 0 0 2-1.7L21.4 8H6"/>
<circle cx="10" cy="20" r="1.4"/>
<circle cx="17" cy="20" r="1.4"/>
</symbol>
</svg>
<svg class="icon" width="24" height="24" aria-hidden="true">
<use href="#icon-cart"/>
</svg>
Элемент symbol хранит фигуру без отрисовки, use достаёт её по идентификатору и рисует с нужным размером. Стандарт SVG 2 упростил адресацию: вместо громоздкого xlink:href достаточно короткого href, и современные браузеры понимают оба варианта. Внешний вариант держит спрайт отдельным файлом: запись use href="/sprite.svg#icon-cart" тянет символ по сети, файл кэшируется один раз и работает на всех страницах, при условии совпадения схемы и порта с текущим документом.
Раскраска решается одним словом: fill со значением currentColor заставляет иконку наследовать цвет текста, поэтому один и тот же значок живёт в тёмной теме, в кнопке и в подсказке. Приём работает для инлайновых вставок и спрайтов, а через тег img внешние стили внутрь картинки не проходят, там цвет зашивают в сам файл. Сборкой занимаются готовые инструменты: пакет svg-sprite версии 2.0.4 с примерно 109 тысячами загрузок в неделю собирает спрайт из папки исходников, а @svgr/core 8.1.0, около 9,8 миллиона загрузок в неделю, превращает SVG в компоненты React, прогоняя чистку внутри конвейера.
Заголовки сервера, сжатие при отдаче и долгий кэш векторных файлов
SVG - это XML-текст, и на уровне сервера он отдаётся как текст: сжатие gzip или brotli уменьшает такой файл в несколько раз, потому что повторяющиеся теги и атрибуты жмутся лучше всего. Заголовок Content-Type со значением image/svg+xml обязателен, иначе браузер предложит скачать файл вместо показа. Настройка для nginx занимает четыре строки:
location ~* \.svg$ {
gzip on;
gzip_types image/svg+xml;
add_header Cache-Control "public, max-age=31536000, immutable";
}
Годовой кэш с флагом immutable безопасен при версионировании имён: файл с новым содержимым получает новое имя, старое остаётся в кэше до истечения срока. Заранее сжатые файлы с расширением svgz требуют точного заголовка Content-Encoding и легко ломаются на промежуточных прокси, поэтому на практике проще отдавать обычный SVG с включённым на сервере сжатием.
Доступность, анимация и уважение к настройкам движения
Векторная графика несёт смысл, и смысл обязан дойти до программ экранного чтения. Значимой картинке нужны роль и подписи: роль img связывает элемент с подписью через aria-labelledby, а элементы title и desc дают короткое имя и развёрнутое описание.
<svg viewBox="0 0 24 24" role="img" aria-labelledby="spin-title spin-desc">
<title id="spin-title">Данные загружаются</title>
<desc id="spin-desc">Кольцо вращается, пока страница готовит ответ</desc>
<circle cx="12" cy="12" r="9" fill="none" stroke="currentColor"
stroke-width="3" stroke-dasharray="42 14">
<animateTransform attributeName="transform" type="rotate"
from="0 12 12" to="360 12 12" dur="1.6s"
repeatCount="indefinite"/>
</circle>
</svg>
Декоративные значки помечают противоположным способом: aria-hidden со значением true прячет их от вспомогательных технологий. Движение имеет границы: в системах существует режим уменьшенного движения, и уважение к нему описывается медиазапросом prefers-reduced-motion:
.progress-ring {
animation: spin 1.6s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
.progress-ring {
animation: none;
}
}
Коварная деталь в том, что медиазапрос останавливает CSS-анимации, но трогать SMIL-элементы вроде animateTransform стилями нельзя. Для полного контроля анимацию переносят в стили, а остановку встроенной SMIL-анимации выполняет метод pauseAnimations у корневого элемента svg.
Сравнение с растровыми форматами и выбор под конкретную задачу
Вес страниц растёт десятилетие подряд, и вектор тут не главная причина роста: медианная десктопная страница за десять лет потяжелела на 1,4 MB, или на 120 процентов, мобильная на 1,8 MB, или на 357 процентов. Разложение медианной страницы по типам ресурсов показывает главного потребителя трафика: 1 054 KB изображений против 613 KB скриптов, 131 KB шрифтов, 78 KB стилей и 18 KB разметки.
Правила выбора формата сводятся к короткому чек-листу:
- иконки интерфейса, логотипы, схемы и диаграммы уходят в SVG после чистки SVGO;
- фотографии и сложные сцены остаются в WebP или AVIF с набором плотностей и атрибутом srcset;
- декоративные градиенты и однотонные плашки рисуются средствами CSS без отдельных файлов;
- смешанные баннеры с фотографией и векторным текстом собирают через
<picture>с растровой запасной версией.
Стандарт под эти задачи обновился недавно: спецификация SVG 2 носит статус Candidate Recommendation Snapshot от 6 октября 2026 года и постепенно протаскивает в практику упрощённые ссылки, новые примитивы и тесную интеграцию с CSS. Ядро рабочей совместимости старше: второе издание SVG 1.1 стало рекомендацией W3C 16 августа 2011 года, базовый набор формата поддерживают около 97 процентов браузеров, и отличия между версиями на практике короче, чем кажется. Вектор не всесилен: карта на тысячи узлов или фотографичная иллюстрация с текстурой на весь экран съест и килобайты, и процессорное время на отрисовку. Для интерфейсной графики формула рабочая: чистый исходник, живой viewBox, спрайт с уникальными идентификаторами, годовой кэш и сжатие на отдаче. Такой конвейер превращает десятки килобайт на иконку в единицы и держит резкость от маленького экрана до большого монитора.