Изображения: главный вес страницы
Картинки — самый тяжёлый тип ресурса на средней странице и обычно тот самый элемент, по которому браузер меряет LCP. Разберём, как отдать их в разы легче и при этом не испортить метрику.
LCP-картинка — изображение, которое браузер счёл самым крупным видимым элементом первого экрана. Скорость её загрузки почти всегда и есть ваш Largest Contentful Paint.
Зачем вообще этим заниматься
Откройте DevTools, вкладку Network, поставьте фильтр Img и посмотрите на сумму внизу. По данным HTTP Archive изображения год за годом держат первое место по весу среди всех типов ресурсов — порядка мегабайта на медианной странице. JavaScript вреднее для отзывчивости, но по байтам лидируют картинки.
И это самая дешёвая оптимизация из всех, что есть. Не нужно переписывать код, менять фреймворк и спорить с бэкендом. Нужно поменять формат, добавить пару атрибутов и один раз настроить сборку. Типичный результат на реальном проекте: hero-баннер был 1,8 МБ PNG на 4000 пикселей ширины, стал 90 КБ AVIF на 1600 — LCP упал с 4,2 до 1,6 секунды.
Почти весь лишний вес берётся из четырёх ошибок: фотографию сохранили в PNG, отдают одну картинку на все экраны, не сжали, не зарезервировали место. Разберём по порядку.
Форматы: что чем заменять
| Формат | Для чего | Вес относительно JPEG |
| JPEG | фото, если нужен максимально совместимый фолбэк | базовая точка отсчёта |
| PNG | только скриншоты интерфейса, пиксель-арт, прозрачность без градиентов | для фото — в разы тяжелее, так делать нельзя |
| WebP | универсальная замена JPEG и PNG | обычно на 25–35% легче |
| AVIF | фото и сложные градиенты, лучшее сжатие | часто вдвое легче при том же качестве |
| SVG | иконки, логотипы, схемы | векторный, вес зависит от числа узлов |
WebP и AVIF поддерживают все актуальные браузеры, так что <picture> с фолбэком нужен уже скорее для спокойствия, чем по необходимости. Но если хочется страховки — вот честная запись, где браузер сам берёт первый формат, который понимает:
<picture>
<source srcset="/img/hero.avif" type="image/avif">
<source srcset="/img/hero.webp" type="image/webp">
<img src="/img/hero.jpg" width="1200" height="675" alt="Панель управления сервисом">
</picture>
Порядок <source> важен: браузер идёт сверху вниз и останавливается на первом подходящем type. Поставите WebP выше AVIF — AVIF никто никогда не увидит.
Адаптивные картинки: srcset и sizes
Одна картинка на все устройства — это либо мыло на ретине, либо 1600 пикселей, отправленные на телефон, где место под неё 360. Решение — дать браузеру набор вариантов и подсказать, какой ширины будет слот.
<img
src="/img/hero-800.jpg"
srcset="/img/hero-400.jpg 400w,
/img/hero-800.jpg 800w,
/img/hero-1600.jpg 1600w"
sizes="(max-width: 600px) 100vw, 800px"
width="800"
height="450"
fetchpriority="high"
alt="Скриншот дашборда с графиком продаж">
Построчно:
srcsetс дескрипторомw— список файлов и их настоящая ширина в пикселях. Не «размер на экране», а ширина самого файла.sizes— обещание браузеру: «на экранах уже 600px картинка займёт всю ширину окна, иначе — ровно 800 CSS-пикселей».widthиheight— резерв места, про них ниже.fetchpriority="high"— «это герой, качай первым».src— фолбэк для древних браузеров, туда кладём средний вариант.
Дальше браузер считает сам: берёт ширину слота из sizes, умножает на плотность экрана (DPR) и выбирает первого кандидата, который не меньше нужного. Логику легко воспроизвести — запустите этот код и посмотрите, что выберется:
const candidates = [400, 800, 1600];
function pick(cssWidth, dpr) {
const need = cssWidth * dpr;
return candidates.find((w) => w >= need) ?? candidates[candidates.length - 1];
}
console.log(pick(360, 2)); // телефон с ретиной: нужно 720
console.log(pick(800, 1)); // обычный ноутбук
console.log(pick(800, 2)); // ноутбук с ретиной
console.log(pick(1200, 3)); // ничего подходящего нет, берём максимум
Результат:
800
800
1600
1600
Отсюда главная ловушка sizes: если вы соврали браузеру, он честно скачает не то. Классика — оставить sizes="100vw" на картинке, которая на самом деле лежит в колонке шириной 320px. На ноутбуке с ретиной браузер посчитает 1440 × 2 = 2880 и утащит самый большой файл, чтобы показать его в четверть размера.
Ленивая загрузка — и почему не для героя
Атрибут loading="lazy" откладывает запрос, пока картинка не окажется недалеко от области видимости. Одна строчка, поддерживают все браузеры, и на длинной странице она экономит мегабайты:
<img src="/img/otzyv-3.webp" width="600" height="400"
loading="lazy" decoding="async" alt="Отзыв клиента">
А теперь — почему её категорически нельзя вешать на LCP-картинку, хотя соблазн «полайзить всё разом» огромен.
Браузер начинает качать ресурсы ещё до того, как построит DOM: за это отвечает preload scanner — он бежит по сырому HTML и вытаскивает адреса картинок, скриптов и стилей. Но loading="lazy" запрещает ему трогать такую картинку: решение «пора грузить» принимается только после того, как посчитан layout, а layout невозможен без CSS. В итоге вы своими руками отодвигаете запрос героя на конец цепочки. На реальных проектах это стабильно 300–800 мс к LCP.
Правило простое: всё, что попадает в первый экран, грузится eager (это значение по умолчанию, писать его не нужно), герою добавляем fetchpriority="high". Всё, что ниже сгиба, — lazy.
Размеры: как не заработать CLS
Если у <img> нет размеров, до загрузки файла браузер считает его высоту нулевой. Картинка приезжает — и весь текст под ней прыгает вниз. Это Cumulative Layout Shift, и лечится он атрибутами width и height: браузер выводит из них aspect-ratio и держит место заранее.
Атрибуты не мешают адаптивности, если в CSS есть привычная пара строк:
img {
max-width: 100%;
height: auto;
}
Без height: auto атрибут высоты зафиксирует картинку жёстко и она сплющится. Ставьте настоящие пропорции исходника: width="800" height="450" для 16:9, а не круглые числа наугад.
Сжатие: конкретные команды
Качество 100 не нужно никому. Для фотографий комфортный коридор — 75–82; для картинок под ретину (которые всё равно ужимаются вдвое) можно смело брать 60, разницы не увидит никто.
# WebP
cwebp -q 80 hero.jpg -o hero.webp
# AVIF (кодировщик из libavif)
avifenc --min 20 --max 30 hero.jpg hero.avif
# Пакетный ресайз + конвертация через sharp-cli
npx sharp-cli -i "src/img/*.jpg" -o dist/img --format webp --width 1600
В нормальном проекте это делает не человек, а сборка: next/image, nuxt/image, imgproxy, Cloudinary — любой из них умеет отдавать нужный формат и размер по запросу. Ручные команды нужны, чтобы понимать, что именно происходит под капотом, и чтобы быстро починить одну конкретную картинку.
Как это работает
Сложите картину целиком. Браузер получает первые байты HTML и запускает preload scanner — тот находит <img> с src и srcset и ставит запросы в очередь ещё до парсинга CSS и выполнения JS. Приоритет всех картинок изначально низкий; после первого layout браузер поднимает приоритет тем, что попали во вьюпорт. fetchpriority="high" просто сообщает об этом сразу, не дожидаясь layout.
Из этой механики следуют два вывода, которые ломают LCP чаще всего. Первый: картинка, заданная в CSS как background-image, для preload scanner не существует — он читает HTML, а не стили. Герой на фоновой картинке всегда медленнее героя на <img>. Второй: картинка, которую вставляет JS (карусель, ленивый компонент), тоже невидима для сканера и грузится в самом конце.
Отсюда практическое следствие: LCP-элемент должен быть обычным <img> прямо в разметке. Если по каким-то причинам этого не избежать, поставьте хотя бы <link rel="preload" as="image"> в <head>.
Частые ошибки
loading="lazy"на hero-картинке — самый дорогой однострочник в вебе.- Герой сделан фоном через CSS: preload scanner его не видит, LCP уезжает на секунду.
sizes="100vw"у картинки в узкой колонке — браузер тянет вариант вчетверо тяжелее нужного.- Конвертировали в WebP, но забыли уменьшить: 4000 пикселей ширины в слоте на 600.
- Фотография в PNG. Скриншот интерфейса в PNG — нормально, фотография — никогда.
- Нет
width/height— получаете CLS на пустом месте. - Иконки как PNG-спрайты вместо SVG или, того хуже, как отдельные запросы по 3 КБ каждый.
fetchpriority="high"навешан на пять картинок сразу: если приоритетно всё, приоритетно ничто.
Итоги
- Картинки — самый большой и самый дешёвый в исправлении вес страницы.
- AVIF или WebP вместо JPEG/PNG дают минус 30–50% байт без единой строчки кода.
srcset+ честныйsizesотдают каждому устройству свой размер; соврать вsizesхуже, чем не писать его вовсе.- Ниже сгиба —
loading="lazy". В первом экране — никогда, тамfetchpriority="high". widthиheightв разметке — бесплатная защита от CLS.- LCP-элемент держите обычным
<img>в HTML, а не фоном в CSS и не вставкой из JS.