Опубліковано: · автор: kodewright
Base64-зображення і SEO: коли data URI шкодять сайту
Вбудовування зображень як Base64 data URI рідко пришвидшує сторінку. Виміряний оверхед, втрати на кешуванні й LCP та випадки, коли інлайн виправданий.
Вбудовування зображень як Base64 data URI у 2026 році майже ніколи не пришвидшує сторінку. Воно має сенс для іконок до кількох кілобайтів і для розмитих плейсхолдерів. Hero-зображення, фото товару чи будь-що, що ви хочете бачити в Google Images, має лишатися звичайним файлом із власною URL-адресою. Аргумент «менше HTTP-запитів» з'явився в часи HTTP/1.1, а мультиплексування в HTTP/2 прибрало більшу частину тієї ціни, з якою він боровся.
Що таке Base64, я тут не пояснюю (про це є гайд про кодування і шифрування). Ця стаття про те, що інлайн робить із байтами, кешуванням і Core Web Vitals, з вимірами, щоб ви могли вирішувати для кожного ресурсу окремо.
Що насправді таке data URI
Data URI вміщує байти файлу просто в URL. Синтаксис за MDN:
data:[<media-type>][;base64],<data>
<img src="data:image/webp;base64,UklGRj4+AABXRUJQVlA4IDI+AAC..." alt="...">
Браузер декодує рядок і малює зображення без мережевого запиту. Це єдина перевага. Усі компроміси випливають із того, що зображення тепер живе всередині іншого файлу.
Оверхед у 33% майже зникає після gzip
Base64 перетворює 3 байти на 4 ASCII-символи (RFC 4648), тому сирий рядок приблизно на третину більший. На цьому більшість гайдів і зупиняються. Але сирий рядок ніхто не передає: він лежить усередині HTML або CSS, а їх сервер стискає. Я поміряв справжнє hero-зображення WebP 1200×630 із цього блогу та SVG-іконку замка 24 px на macOS:
base64 -i hero.webp | tr -d '\n' | wc -c # розмір у Base64
base64 -i hero.webp | tr -d '\n' | gzip -9c | wc -c # скільки реально йде мережею
| Ресурс | Оригінал | Base64 | Оригінал, gzip -9 | Base64, gzip -9 |
|---|---|---|---|---|
| hero.webp (1200×630) | 32 436 Б | 43 248 Б (+33%) | 32 474 Б | 32 765 Б (+1%) |
| icon.svg (24×24) | 269 Б | 360 Б (+34%) | 207 Б | 289 Б (+40%) |
На вже стиснутих бінарних форматах gzip забирає майже весь оверхед Base64, тож по мережі hero коштує десь на 1% більше. Якби вся справа була в обсязі передачі, інлайн цього hero був би майже безкоштовним.
З SVG усе навпаки. SVG — це текст, який і сам добре стискається, а Base64 руйнує повтори, на які спирається gzip, тож вбудована версія лишається на 40% важчою навіть після стиснення. Саме тому SVG варто кодувати як URL, а не як Base64 (про це нижче).
Де інлайн справді вам коштує
Кешування зникає
Файл /images/logo.webp завантажується один раз, а на наступних сторінках береться з кешу. Data URI кешується лише разом із файлом, у якому лежить. Якщо це HTML, а SSR-HTML зазвичай віддають із no-cache або коротким max-age, зображення завантажується заново на кожній сторінці. Вбудуйте логотип на 4 КБ у лейаут, і відвідувач, який переглянув 6 сторінок, завантажить його 6 разів: 24 КБ замість 4.
Інлайн у зовнішній CSS-файл менш шкідливий, бо таблиця стилів кешується. Але тоді будь-яка зміна зображення інвалідує весь файл стилів, і кожен користувач заново завантажує весь ваш CSS через правку однієї іконки.
Байти, що блокують рендеринг
CSS блокує рендеринг. Кожен кілобайт Base64 у таблиці стилів браузер мусить завантажити й розпарсити, перш ніж щось намалювати, зокрема текст, який зазвичай і є вашим елементом LCP. Звичайний <img> preload scanner завантажує паралельно, і перший рендер на нього не чекає.
З HTML те саме: вбудовані зображення відсувають усе, що йде після них, далі в потоці байтів. Data URI на 40 КБ у <head> (від криво налаштованого конструктора сторінок або фреймворку, що вбудував OG-зображення) затримує кожен байт контенту після нього.
LCP стає гіршим, а не кращим
Якщо елемент LCP — це вбудоване зображення, воно не може з'явитися раніше, ніж HTML, у якому воно лежить, буде отримано й розпарсено. Йому також не допоможуть fetchpriority="high" чи <link rel="preload">, а loading="lazy" нічого не економить, бо байти вже в документі. Звичайне зображення з fetchpriority="high" на реальному з'єднанні зазвичай виграє: текст малюється, щойно прийшли HTML і CSS, а preload scanner тим часом паралельно тягне hero.

Поріг Google для «доброго» LCP — 2,5 секунди на 75-му перцентилі. Звіряйтеся зі своїми польовими даними, перш ніж вірити будь-якому емпіричному правилу, зокрема моєму.
Сигнали для пошуку зображень втрачаються
Документація Google щодо SEO зображень справді дозволяє вбудовувати зображення як data URI в <img src> і додає, що інлайн «може суттєво збільшити розмір сторінки». На тій самій сторінці сказано, що Google не індексує CSS-зображення, тож background-image: url(data:...) для пошуку зображень невидимий у будь-якому разі. Моя думка щодо data URI в <img>: на зображення без сталої URL-адреси не можна послатися, його не можна поширити чи додати в sitemap зображень, тож власних сигналів воно не набирає. Для фото, яке має ранжуватися, цього достатньо, щоб лишити його файлом.
Є ще й жорсткий ліміт розміру. Googlebot обробляє лише перші 2 МБ HTML-файлу. Кілька великих вбудованих зображень на довгій сторінці можуть виштовхнути реальний контент за цю межу.
Де інлайн таки окупається
| Ресурс | Вбудовувати? | Чому |
|---|---|---|
| UI-іконки до ~1–2 КБ у CSS | Так, SVG у URL-кодуванні | Економить запит на щось крихітне; текст добре стискається |
| Розмитий LQIP-плейсхолдер (~20×12 px) | Так, Base64 | Показує щось, поки вантажиться справжнє зображення; надто малий, щоб на щось впливати |
| Зображення в підписі листа чи HTML-розсилці | Іноді | Одні клієнти блокують віддалені зображення, інші — data URI. Тестуйте на своїх |
| Логотип у спільному лейауті сайту | Ні | Передається з кожною сторінкою; краще кешуйте |
| Hero / LCP-зображення | Ні | Окремий файл із fetchpriority="high" |
| Фото товарів і статей | Ні | Потрібна стала URL-адреса для пошуку зображень |
| Усе, що більше за 4 КБ | Ні | Втрати на кешуванні й блокуванні рендерингу більші за зекономлений запит |
Щодо плейсхолдерів: починаючи з Chrome 112, зображення, що мають менше ніж 0,05 біта даних на відображений піксель, не враховуються як кандидати в LCP. Крихітний розмитий Base64-плейсхолдер, розтягнутий на всю ширину, опиняється нижче цього порога, тож «підробити» швидкий LCP він не зможе. Справжнє зображення однаково має завантажитися. Плейсхолдер покращує сприйняте завантаження, але метрику не зрушить.
Кодуйте SVG як URL, а не Base64
Для SVG пропустіть Base64 і закодуйте розмітку відсотковим кодуванням:
.icon-lock {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2310b981' stroke-width='2'%3E...%3C/svg%3E");
}
Правила, які мають значення: замініть подвійні лапки всередині SVG на одинарні, щоб вони не закрили CSS-рядок, закодуйте < і > як %3C і %3E і завжди кодуйте # як %23. Незакодований # у hex-кольорі починає фрагмент URL, і браузер відкидає все після нього: класична причина, чому вбудований SVG мовчки нічого не показує. Для іконки замка вище URL-кодована форма зайняла 286 байтів, після gzip — 204, проти 289 для Base64 після gzip.
Вручну це можна зробити в URL кодувальнику / декодувальнику. Якщо SVG — це ще не опублікований брендовий арт, це безпечніше, ніж випадковий онлайн-конвертер: усе працює у вашому браузері, і розмітка не потрапляє на жоден сервер. Для пайплайна збірки те саме робить пакет mini-svg-data-uri, і він закриває крайні випадки.
Перевірте, що вже вбудовує ваша збірка
Можливо, ви вже вбудовуєте зображення, навіть не знаючи про це. У Vite (а отже, і в Nuxt, SvelteKit та Astro) є build.assetsInlineLimit зі значенням за замовчуванням 4096 байтів: будь-який імпортований ресурс, менший за 4 KiB, перетворюється на Base64 data URL у бандлі. Таке значення розумне. Підняти його до 50 КБ, «щоб скоротити запити», означає повернути всі витрати, описані вище.
// nuxt.config.ts
export default defineNuxtConfig({
vite: {
build: {
// залиште значення за замовчуванням або зменште його; 0 вимикає інлайн
assetsInlineLimit: 4096,
},
},
})
Щоб знайти вбудовані зображення на живій сторінці, відкрийте DevTools, вкладку Network і відфільтруйте за data:, або виконайте в консолі:
[...document.querySelectorAll('img[src^="data:"]')]
.map(i => ({ alt: i.alt, kb: (i.src.length / 1024).toFixed(1) }))
Для CSS ця команда виводить розміри найбільших вбудованих ресурсів у результатах збірки (.output/public для Nuxt, dist для чистого Vite):
find .output/public -name '*.css' -exec grep -ho 'url(data:[^)]*' {} + | awk '{ print length }' | sort -n | tail
Усе, що більше за кілька кілобайтів, — кандидат на повернення в окремий файл.
Коли Base64-рядок таки потрібен
Для випадків, що пройшли таблицю вище (плейсхолдер, зображення в листі, тестова фікстура), Base64 кодувальник / декодувальник приймає файл і локально повертає сирий Base64-рядок. Він повертає лише рядок, тож префікс додайте самі: data:image/webp;base64, для WebP, data:image/png;base64, для PNG. Неправильний MIME-тип у Chrome зазвичай усе одно рендериться, але в суворіших клієнтах, особливо поштових, може зламатися.
Якщо паралельно у вас міграція з купою редиректів, гайд про масове URL-кодування розбирає ту саму проблему з боку відсоткового кодування.
Швидкий аудит наявного сайту: запустіть консольний сніпет на трьох основних шаблонах, поверніть кожне вбудоване зображення понад 4 КБ в окремий файл із нормальними атрибутами width, height і fetchpriority та переведіть усі Base64 SVG у вашому CSS на URL-кодування.
FAQ
Чи індексує Google зображення у форматі Base64 data URI?
За власною документацією Google, data URI в <img src> підтримуються. Фонові CSS-зображення не індексуються взагалі, data URI це чи ні. Для зображень, які мають ранжуватися, звичайна URL-адреса файлу все одно кращий вибір: на неї можна послатися, її можна поширити й додати в sitemap зображень.
Чи завантажуються Base64-зображення швидше?
Для іконки на кілька сотень байтів, потрібної для першого рендеру, трохи швидше. Для всього більшого втрата окремого запису в кеші та зайва вага в HTML чи CSS, які блокують рендеринг, переважують один зекономлений запит, особливо на HTTP/2 чи HTTP/3, де додаткові запити дешеві.
Наскільки Base64-зображення більше за оригінал?
Сирий рядок приблизно на 33% більший. Коли відповідь стискається gzip, для вже стиснутих форматів більша частина цього оверхеду зникає: у моєму тесті з WebP він упав із +33% до приблизно +1%. Думаю, JPEG і PNG поводяться схоже, але поміряйте свої файли: закодуйте один у Base64 кодувальнику / декодувальнику і порівняйте довжину з розміром оригіналу. Текстові формати на кшталт SVG зберігають більшу частину оверхеду, тому їх краще кодувати як URL.
Який ліміт інлайну за замовчуванням у Vite?
4096 байтів. Ресурси, які ви імпортуєте і які менші за цей розмір, потрапляють у JS- або CSS-бандл як Base64. Встановіть build.assetsInlineLimit: 0, щоб вимкнути інлайн, або передайте функцію, щоб вирішувати для кожного файлу окремо.
Чи можна використати data URI в тегу Open Graph зображення?
Ні. Facebook, LinkedIn, Slack і X завантажують og:image своїми краулерами за абсолютною URL-адресою. Data URI вони завантажити не можуть, тож посилання поширюється без прев'ю. Згенеруйте справжній файл 1200×630 і вкажіть у тегу його.