Во что обходится data URI
Data URI помещает файл прямо в URL: data:image/png;base64,iVBORw0KGgo.... Его можно подставить везде, где ожидается URL, так что иконка может жить в атрибуте src в HTML или в background-image в CSS, и браузер никогда не сделает за ней отдельный запрос.
Компромиссы такие:
- Размер. Base64 увеличивает данные до 4/3 от исходного объёма, примерно на 33% (точную формулу см. в статье о росте размера в Base64). Префикс
data:image/png;base64,добавляет ещё 22 байта. - Кеширование. Встроенный файл кешируется только в составе документа, в котором он лежит. Логотип, встроенный в каждую HTML-страницу, скачивается заново с каждой страницей. Встроенный в CSS-файл, он кешируется вместе с этим CSS, и замена одной иконки сбрасывает кеш всей таблицы стилей.
- Рендеринг. Браузер ничего не отрисует, пока не загрузится блокирующий рендеринг CSS. Таблица стилей с 200 КБ встроенных шрифтов или картинок откладывает первую отрисовку ровно на то время, которое нужно на загрузку этих байтов. В HTML встроенные данные отодвигают дальше по потоку байтов весь контент после них.
- Никакой ленивой загрузки.
loading="lazy"на<img>с data URI ничего не экономит: байты уже пришли вместе со страницей.
Взамен вы экономите запрос. В HTTP/1.1 это имело большое значение, потому что браузеры открывали к одному хосту лишь несколько соединений. В HTTP/2 и HTTP/3 множество запросов делят одно соединение, так что выигрыш меньше: в основном заголовки запроса и ответа плюс одна задержка на путь туда и обратно (RTT) для файлов, которые браузер обнаруживает поздно.
Замеры: что gzip и Brotli делают с Base64
Цифра 33% относится к несжатому тексту. Большая часть HTML и CSS отдаётся с gzip или Brotli, поэтому важен размер после сжатия. Мы измерили его сами.
Методика. Файлы-образцы из пакетов Debian 13: PNG-иконка 16×16 (тема иконок HighContrast), PNG 48×48 с RGBA (тема hicolor), синие обои Xfce в JPEG 2880×1800, уменьшенный из них в Pillow JPEG 800×500 с качеством 80, шрифт Roboto Slab Regular в WOFF2 и две SVG-иконки (символьная иконка Adwaita на 479 байт и завиток Debian из GNOME на 7,6 КБ). Каждый файл и его Base64 (только данные, без префикса data:) сжимались через zlib из Node.js 24: gzip с уровнем 9 и Brotli с качеством 11 (обычные настройки для предварительно сжатой статики). Размеры указаны в байтах.
| Файл | Исходный | Base64 | Исходный, gzip | Base64, gzip | Исходный, Brotli | Base64, Brotli |
|---|---|---|---|---|---|---|
| PNG 16×16 | 281 | 376 | 285 | 306 | 285 | 284 |
| PNG 48×48 | 4552 | 6072 | 4575 | 4602 | 4551 | 4589 |
| JPEG 800×500 | 8195 | 10 928 | 7425 | 7173 | 7160 | 6908 |
| JPEG 2880×1800 | 197 011 | 262 684 | 182 684 | 183 945 | 180 114 | 181 140 |
| Шрифт WOFF2 | 52 532 | 70 044 | 52 538 | 52 765 | 52 536 | 52 690 |
| SVG-иконка | 479 | 640 | 291 | 427 | 247 | 410 |
| SVG-завиток | 7610 | 10 148 | 3123 | 4398 | 2582 | 3823 |
Что показывают цифры:
Уже сжатые двоичные файлы теряют почти весь лишний объём. Данные PNG, JPEG и WOFF2 уже сжаты и близки к случайным, поэтому повторное сжатие почти ничего не даёт (оба PNG под gzip даже немного выросли). Base64 от случайных байтов использует 64 символа по 8 бит, а кодирование Хаффмана возвращает это примерно к 6 битам на символ. Итог: сжатый Base64 занял от 96% до 107% от сжатого двоичного файла и от 84% до 109% от исходного. Сами JPEG здесь немного поддавались сжатию, поэтому их Base64-версии даже вышли чуть меньше, чем JPEG под gzip; разница слишком мала, чтобы на неё рассчитывать. На крошечном PNG 16×16 фиксированные 18 байт заголовка и хвоста gzip выглядят как большой процент, а более лёгкая обвязка Brotli это стёрла.
SVG становится хуже. В SVG-тексте куча повторов, и сжатие его обожает: завиток уменьшился под gzip с 7610 до 3123 байт. Base64 перекраивает каждую 3-байтную группу в 4 символа, значения которых зависят от выравнивания, так что повторяющаяся подстрока уже не выглядит одинаково дважды. Сжатый gzip-ом Base64 SVG был на 41–47% больше сжатого SVG, а с Brotli на 48–66%.
SVG в URL-кодировании против Base64
SVG вообще не нуждается в Base64: это и так текст. Data URI с URL-кодированием оставляет его читаемым:
.flag {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3 0v16h2V8h3l1 2h5V2h-4L9 0z'/%3E%3C/svg%3E");
}
Экранировать нужно как минимум %, # (он начал бы фрагмент URL), < и >, плюс ту кавычку, которой ограничен атрибут. Если заменить двойные кавычки внутри SVG на одинарные, их экранировать не придётся. Символы вне ASCII нужно закодировать в процентной форме как UTF-8.
Те же два SVG, измеренные тем же способом:
| SVG | Кодирование | Размер | Gzip | Brotli |
|---|---|---|---|---|
| Иконка | Base64 | 640 | 427 | 410 |
| Иконка | Минимальное URL-кодирование | 493 | 293 | 253 |
| Иконка | encodeURIComponent | 723 | 326 | 279 |
| Завиток | Base64 | 10 148 | 4398 | 3823 |
| Завиток | Минимальное URL-кодирование | 7179 | 3057 | 2549 |
| Завиток | encodeURIComponent | 11 594 | 3319 | 2750 |
«Минимальное» означает: пробельные символы схлопнуты, двойные кавычки заменены на одинарные, экранированы только %, #, <, >. Такой вариант сжался примерно до размера исходного SVG, а завиток даже чуть меньше, потому что схлопывание пробелов убрало часть байтов. Полный encodeURIComponent больше в несжатом виде, но всё равно сжимается намного лучше Base64. Инструмент SVG в data URI выдаёт обе формы рядом.
Base64 для SVG по-прежнему оправдан, когда инструмент или API принимает только Base64 или когда в SVG есть символы, об экранировании которых вам не хочется думать.
Content Security Policy и data:
В Content Security Policy 'self' не включает URL со схемой data:. Каждая директива, которая должна их разрешать, обязана перечислить эту схему:
Content-Security-Policy: default-src 'self'; img-src 'self' data:; font-src 'self' data:
Разрешить data: в img-src и font-src считается обычной практикой с низким риском. С script-src, default-src или object-src всё иначе: если разрешить data: там, злоумышленник, способный внедрить разметку, сможет подсунуть скрипт без всякого внешнего хоста, и CSP во многом теряет смысл. Если политика мешает встроенным картинкам, расширяйте только img-src.
Кроме того, браузеры запрещают страницам переводить окно верхнего уровня на URL data: ссылкой, редиректом или скриптом. Поэтому data URI работает как встроенный ресурс, но не как цель ссылки.
Когда встраивание оправдано
Обычно оправдано:
- Крошечные иконки и картинки интерфейса примерно до 1–2 КБ, особенно нужные для первой отрисовки. Накладные расходы на запрос составляют большую долю их размера, а сжатие стирает почти всю цену Base64.
- HTML одним файлом. Офлайн-отчёты, выгруженная документация, страницы покрытия тестами и превью HTML-писем, которые должны работать при открытии с диска.
- Критический CSS, который и так встроен в HTML, и маленькая фоновая картинка едет вместе с ним.
- Сгенерированный контент, например график, отрисованный скриптом в PNG и вставленный в HTML-отчёт.
Обычно не оправдано:
- Фотографии и всё, что больше нескольких килобайт. Им лучше быть отдельными файлами с долгим сроком кеширования.
- Картинки, общие для многих страниц: они будут заново скачиваться внутри каждого HTML-ответа.
- Веб-шрифты. Файл WOFF2 обычно весит десятки килобайт (измеренный выше файл весит 52 КБ), встраивание в CSS дольше держит таблицу стилей блокирующей, а отдельные файлы шрифтов можно кешировать и загружать через preload.
- Всё, что ниже первого экрана. Отдельный файл с
loading="lazy"ничего не стоит, пока не понадобится.
Электронная почта стоит особняком. Поддержка картинок data: в почтовых клиентах разная, так что проверяйте в тех клиентах, которыми пользуются ваши получатели. Надёжнее передать картинку в HTML-письме как встроенное MIME-вложение со ссылкой через cid:; это тоже Base64, только внутри структуры MIME. Об этом подробнее рассказано в статье Base64 в электронной почте.
Перетащите PNG, JPEG, WebP или SVG и получите data URI и его размер. Файл остаётся в вашем браузере.
Вопросы и ответы
Картинки в Base64 медленнее обычных?
Сами по себе нет. Маленькие встроенные картинки избавляют от запроса, а большие раздувают HTML или CSS, не кешируются отдельно и могут задерживать рендеринг. До нескольких килобайт встраивание помогает, дальше вредит.
Убирает ли gzip 33% лишнего объёма Base64?
Для PNG, JPEG и WOFF2 почти полностью: в наших замерах сжатый gzip-ом Base64 оказывался в пределах нескольких процентов от двоичного файла. Для SVG и другого текста нет: сжатый Base64 SVG был на 41–47% больше сжатого SVG.
Для встроенного SVG лучше URL-кодирование или Base64?
URL-кодирование. Оно читаемое, редактируемое и сжимается примерно так же, как исходный SVG. Экранируйте как минимум #, %, < и >, а внутри SVG используйте одинарные кавычки, если атрибут заключён в двойные.
Почему Content Security Policy блокирует мои картинки в data URI?
CSP считает data: отдельной схемой, которую 'self' не покрывает. Добавьте data: в img-src (или в font-src для шрифтов). В script-src и default-src его не добавляйте.