Оптимизация изображений на сайте: приёмы CSS и JavaScript
Картинки чаще всего тяжелее всего остального на странице. Разбираем, какие приёмы в HTML, CSS и JS реально снижают вес и сдвигают LCP, а какие тратят время впустую.

На типичной странице изображения дают больше половины веса — по данным HTTP Archive, медианный вес картинок на десктопе стабильно превышает вес скриптов и стилей. При этом именно картинка чаще всего оказывается элементом LCP (Largest Contentful Paint), а значит напрямую влияет на оценку Core Web Vitals. Ниже — приёмы, которые работают на практике, с честной ценой каждого: что выигрываете и чем платите.
Задача и почему очевидное решение подводит
Очевидное решение — прогнать все PNG и JPEG через сжатие и радоваться. Оно уменьшает вес, но не трогает две другие проблемы: браузер грузит картинку не того размера, что нужно устройству, и грузит её тогда, когда она ещё не нужна. Картинка 3000×2000 в контейнере 600px по ширине — это мегабайты, потраченные впустую, даже если она идеально сжата.
Поэтому оптимизация изображений — это три независимых рычага: формат и сжатие, адаптивная выдача под размер контейнера, и управление приоритетом загрузки. Их часто путают и делают только первый.
Термин. LCP (Largest Contentful Paint) — время, за которое отрисовывается самый крупный видимый элемент в области первого экрана. Часто это как раз главное изображение или баннер. Хорошим считается значение до 2,5 секунды.
Рычаг 1. Формат и сжатие
WebP и AVIF дают ощутимо меньший вес при том же визуальном качестве, чем JPEG. AVIF обычно сжимает сильнее WebP, но кодирование медленнее, а поддержка чуть уже. Оба формата поддерживаются всеми актуальными версиями Chrome, Firefox, Safari и Edge — но старые версии Safari до 16 не знают AVIF, поэтому фолбэк нужен.
Правильный способ отдать современный формат с откатом на старый — элемент <picture>. Браузер берёт первый источник, который умеет декодировать:
<picture>
<source srcset="hero.avif" type="image/avif">
<source srcset="hero.webp" type="image/webp">
<img src="hero.jpg" alt="Вид на набережную" width="1200" height="675">
</picture>
Порядок важен: AVIF первым, WebP вторым, JPEG в <img> как последний фолбэк. Атрибуты width и height обязательны — без них браузер не знает пропорции до загрузки и получаете сдвиг макета (CLS).
Рычаг 2. Адаптивная выдача под размер
Второй рычаг — не отдавать телефону картинку в разрешении для 4K-монитора. Атрибуты srcset и sizes дают браузеру набор вариантов и правило, как выбрать нужный.
<img
src="photo-800.jpg"
srcset="photo-400.jpg 400w,
photo-800.jpg 800w,
photo-1600.jpg 1600w"
sizes="(max-width: 600px) 100vw, 600px"
width="800" height="600"
alt="Интерьер студии">
Здесь srcset перечисляет файлы с их реальной шириной в пикселях, а sizes объясняет: на экранах уже 600px картинка занимает всю ширину окна, иначе — фиксированные 600px. Браузер сам учтёт и это правило, и плотность пикселей устройства, и выберет минимально достаточный файл.
Цена: вам нужно генерировать несколько размеров каждого изображения. Это делается на этапе сборки или на лету CDN, но вручную поддерживать три-четыре файла на картинку нереально — нужен автоматический пайплайн.
Рычаг 3. Приоритет загрузки
Картинки за пределами первого экрана можно грузить лениво. Нативный ленивый импорт встроен в браузеры и не требует JavaScript:
<img src="gallery-item.webp" loading="lazy"
width="400" height="300" alt="Фото из галереи">
А вот на LCP-изображение loading="lazy" ставить нельзя — это распространённая ошибка, которая специально замедляет главную картинку. Для неё наоборот повышают приоритет:
<img src="hero.webp" fetchpriority="high"
width="1200" height="675" alt="Главный баннер">
Атрибут fetchpriority поддерживается в Chrome и Edge с 101-й версии и в Safari с 17-й; в остальных браузерах он игнорируется без вреда. Ещё сильнее ускоряет LCP предзагрузка в <head>:
<link rel="preload" as="image" href="hero.webp"
fetchpriority="high">
CSS: где картинки прячутся и где текут ресурсы
Фоновые изображения через background-image браузер не грузит, пока не построит дерево рендеринга и не поймёт, что элемент виден. Это иногда полезно (декоративный фон не мешает первому экрану), а иногда вредит: если фон и есть LCP-элемент, он загрузится поздно, потому что CSS обнаружит его позже, чем парсер нашёл бы обычный <img>.
Практический вывод: значимые картинки — это <img> или <picture> в разметке, а не CSS-фон. Фон оставляйте для декора.
Полезные CSS-приёмы, которые не про вес, а про поведение:
aspect-ratio: 16 / 9на контейнере резервирует место и убирает сдвиг макета, даже еслиwidth/heightне проставлены;object-fit: coverобрезает картинку под контейнер без искажения пропорций — заменяет ручную обрезку в редакторе для многих случаев;content-visibility: autoна длинных блоках с картинками откладывает рендеринг того, что за экраном, и снижает время первой отрисовки.
Самая частая и самая дешёвая ошибка — отсутствие width и height. Она стоит вам CLS на каждой странице, а лечится двумя атрибутами, которые вы и так знаете.
JavaScript: когда он нужен, а когда мешает
Нативный loading="lazy" закрыл почти все сценарии, ради которых раньше подключали JS-библиотеки ленивой загрузки. Свой JavaScript оправдан в двух случаях:
- нужна тонкая логика подгрузки — например, прогрев следующей картинки карусели заранее;
- нужен контроль момента загрузки, которого нет в нативном API — тогда используют
IntersectionObserver, а не слушательscroll.
Слушатель scroll для отслеживания видимости — устаревший антипаттерн: он срабатывает десятки раз в секунду и грузит главный поток. IntersectionObserver делает то же самое силами браузера:
// Ленивая загрузка через IntersectionObserver
// для случаев, где нативного loading="lazy" мало
const observer = new IntersectionObserver((entries, obs) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
const img = entry.target;
img.src = img.dataset.src; // реальный URL лежит в data-src
obs.unobserve(img); // отписываемся, дело сделано
});
}, { rootMargin: "200px" }); // начинаем грузить за 200px до появления
document.querySelectorAll("img[data-src]")
.forEach((img) => observer.observe(img));
rootMargin в 200px даёт запас: картинка начинает грузиться до того, как попала в кадр, и пользователь не видит пустое место. Обратная сторона JS-подхода: если скрипт не выполнился (ошибка, блокировщик), картинки не появятся вовсе. Нативный loading="lazy" такого риска не несёт — поэтому по умолчанию выбирайте его.
Когда что выбирать
| Задача | Инструмент | Чем платите |
|---|---|---|
| Меньший вес без потери качества | AVIF/WebP через <picture> | Несколько файлов, время кодирования AVIF |
| Не отдавать лишние пиксели | srcset + sizes | Нужен пайплайн генерации размеров |
| Отложить внеэкранные картинки | loading="lazy" | Почти ничего, если не ставить на LCP |
| Ускорить LCP-изображение | fetchpriority + preload | Preload грузит файл рано, экономьте на числе preload |
| Тонкая логика подгрузки | IntersectionObserver | Зависимость от выполнения JS |
Частые ошибки, которые видно в замерах
- loading="lazy" на главном баннере. Браузер откладывает загрузку самого важного элемента, LCP растёт на секунду и больше. Проверяется в Lighthouse — он прямо предупреждает об этом.
- Нет width/height и нет aspect-ratio. Контент прыгает при загрузке картинки, CLS уходит в красную зону.
- Один огромный файл для всех устройств. Телефон качает картинку под десктоп. Ловится, если посмотреть реальный размер запрошенного файла в DevTools на мобильной эмуляции.
- Десяток preload-ссылок. Предзагрузка всего подряд отбирает пропускную способность у по-настоящему важной картинки. Preload — только для LCP.
Что делать дальше
Прогоните ключевые страницы через Lighthouse или PageSpeed Insights и смотрите на два показателя: LCP и CLS. Дальше — по порядку убывания эффекта: сначала проверьте, что LCP-картинка не ленивая и имеет размеры, потом переведите картинки в AVIF/WebP, потом добавьте srcset. Инвалидация и хранение сгенерированных вариантов на CDN — отдельная тема, здесь мы её не разворачиваем. Замеряйте не синтетику, а поле: Core Web Vitals в Search Console показывают реальных пользователей, а не идеальный лабораторный прогон.
Редактор изображений: Убрать лишнее, заменить фон, добавить объект, расширить кадр — на вашем изображении.
Спросить за 20 ₽Источники: web.dev — Optimize images (LCP, срок годности, форматы), MDN — Responsive images (srcset, sizes, picture)
Частые вопросы
AVIF или WebP — что выбрать, если можно только один?
А если fetchpriority не поддерживается в браузере пользователя?
Ленивая загрузка через loading=lazy не сработала, картинки грузятся сразу. Почему?
Стоит ли вообще писать свой JS для ленивой загрузки?
Почему картинку лучше ставить через img, а не background-image в CSS?
Насколько сильно преобразование в AVIF/WebP ускоряет сайт на практике?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.