Обратимся к статистике: по данным с archive.org за последние 6 лет средний размер веб-страницы значительно увеличился. Если в декабре 2019 года средний вес десктопной веб-страницы составлял 1,9 МБ, то сейчас он уже 2,9 МБ для десктопа и 2,6 МБ для мобильных устройств. Это рост на 50% за 6 лет, причём изображения составляют до 40% общего веса страницы.
С увеличением разрешения экранов, усложнением дизайна и тенденцией к использованию изображений высокого качества нагрузка на сайты продолжит расти. Это напрямую влияет на скорость загрузки, потребление трафика и, в конечном итоге, на пользовательский опыт.
Просматривая недавно пул-реквест, я заметил, что туда добавили новые изображения. Меня удивил выбор формата: для картинок без прозрачности использовали PNG. Это кажется избыточным, так как для многоцветных изображений лучше подходят форматы вроде JPEG, которые весят меньше.
Я решил провести эксперимент: оптимизировал и переконвертировал изображения в JPEG, WebP и AVIF, сохранив качество на глаз неотличимым от оригинала.

Результат оказался впечатляющим: размер файлов JPEG уменьшился в 4 раза, а современные форматы WebP и AVIF показали ещё большую экономию. Это заставило меня задуматься: многие разработчики до сих пор не используют современные форматы и не оптимизируют изображения.

Имея внутреннюю тягу к оптимизации и доступности, я решил собрать советы и практики, которые я сам применяю и они помогут разработчикам улучшить пользовательский опыт. Их можно использовать как по отдельности, так и в комплексе.
И давайте начнем с современных форматов изображений
О преимуществах современных форматов вы, вероятно, уже слышали, но давайте кратко их освежим.
О современных форматах
Современные форматы (WebP, AVIF) обеспечивают:
Более эффективное сжатие, чем JPEG и PNG.
Поддержку прозрачности (WebP, AVIF).
Поддержку анимации (WebP).
Продвижение на крупных платформах (Netflix активно использует видеокодек AV1, на котором основан AVIF).
Но у новых форматов есть и недостатки:
AVIF:
Неэффективен для небольших изображений (аватарок, иконок, если не используется SVG), т.к. WebP-кодирование изображений позволяет добиться лучших результатов.
Теряет детали при сильном сжатии, что делает его неподходящим для увеличиваемых изображений (например, в карточках товаров).
Safari начал поддерживать AVIF только недавно.
WebP:
Качество прозрачности хуже, чем у PNG.
Отсутствие поддержки на некоторых устройствах (смарт-ТВ, ридерах).
Важны полупрозрачные края (тени, размытые элементы).
Требуется 100% качество прозрачности (логотипы, UI-элементы).
Нужно минимизировать размер в максимальном качестве (иногда WebP lossless весит больше PNG).
Также проблемы возникают, если в изображении плавные прозрачные градиенты.

Слева исходное изображение в PNG, справа WebP.
JPEG XL — стоит также следить за этим форматом. Он создавался как замена классическому JPEG и превосходит даже AVIF по некоторым параметрам, особенно в сохранении деталей при сжатии и отказоустойчивости. Пока поддержка браузерами невелика (в основном Safari), но в долгосрочной перспективе это главный конкурент за место «универсального формата».
Также хотел обратить внимание, что формат JPEG у нас может быть прогрессивным, это никак не влияет на его размер. «Прогрессивный» в этом случае не является синонимом к слову «современный» — речь идет о прогрессии в её базовом понимании. Вы можете заметить на слайде, как это реализуется. Прогрессивные изображения появляются на страницах сайта моментально, но в плохом качестве. Их качество нарастает постепенно, по мере загрузки сайта, и в итоге доходит до максимума, когда страница полностью прогрузится. Обычные JPEG-изображения появляются на экране постепенно: сначала небольшой кусочек картинки сверху, потом еще больше. Каждый кусок отображается в максимальном разрешении.

Подытожим с выбором формата, какой и для чего использовать:
PNG для изображений с прозрачностью и большим количеством текстовых элементов.
JPEG для фотографий и градиентных изображений.
WebP/AVIF для оптимизации и экономии трафика.
Чтобы обеспечить совместимость со старыми браузерами и при этом использовать современные форматы, применяйте тег <picture>. Внутрь поместите несколько элементов <source> с указанием форматов (например, AVIF, WEBP) в порядке приоритета — браузер выберет первый поддерживаемый. В самом конце добавьте <img> с классическим JPEG/PNG в качестве запасного варианта (fallback). Этот тег сработает, если ни один из <source> не подошёл.
<picture> <source srcset=”image.avif” type=”image/avif”> <source srcset=”image.webp” type=”image/webp”> <img src=”image.png” alt=”Описание изображения”> </picture>
Тогда идем дальше и посмотрим…
Как работать с ретина-экранами
Чтобы понять, чем ретина-экраны отличаются от обычных, разберёмся с пикселями. Они бывают физические и CSS-пиксели.
Физический пиксель — неделимая ячейка на матрице экрана. Например, ширина экрана 480px означает 480 таких ячеек. Они формируют изображение через аддитивное смешение цветов: субпиксели RGB выключаются для чёрного и включаются для белого.
Плотность экрана (PPI, Pixels per Inch) — число физических пикселей на дюйм. Чем выше PPI, тем детальнее картинка.
Device Pixel Ratio (DPR) — отношение физических пикселей к CSS-пикселям. Например, если DPR = 2, это значит, что 1 CSS-пиксель отображается через 2×2 (4 физических пикселя), что делает изображение более чётким.
Почему важен DPR?
Современные экраны с высокой плотностью пиксе��ей используют DPR > 1, поэтому если не учитывать это при разработке, элементы интерфейса могут выглядеть размытыми. Браузеры автоматически масштабируют CSS-пиксели, но разработчикам важно учитывать это при создании графики и адаптивного дизайна.

«Ретина» — маркетинговое название Apple для экранов с высокой плотностью пикселей. Впервые представлена в iPhone 4 (2010, 326 PPI, DPR = 2), она сделала текст и изображения более чёткими, устранив «зернистость».

Часто разработчики, не задумываясь, загружают только одну версию изображения — либо с обычным разрешением (1Х), либо используют одно изображение двукратного размера, чтобы пользователи с ретина-дисплеями видели чёткие изображения. Но давайте посмотрим на статистику. По анализу StatCounter и Steam Hardware Survey можно предположить, что доля пользователей десктопных сайтов с DPR=1 составляет примерно 60-70%. Для обычных дисплеев загрузка 2Х-версий приводит к избыточному трафику, а пользователи ретина-дисплеев видят изображения с недостаточной чёткостью.
Как решить данную проблему? Использовать атрибут srcset для тега <img> и медиа-запросы в CSS для фоновых изображений.
<img src=”small.jpg” srcset=”medium.jpg 1.5x, large.jpg 2x” alt=”Изображение для разных DPR” >
.block { background-image: url(“../img/some-img.png”); } @media (min-resolution: 2dppx) { .block { background-image: url(“../img/some-img@2x.png”) } }
Растровые изображения в векторном формате
Наверняка вы нарывались на закодированные в base64 PNG в векторном формате. Это могло быть даже случайно: дизайнер добавил PNG-изображения, а вы экспортировали его как SVG.

Пример из реальной жизни: дизайнер присылает нам SVG-картинку размером 133×145 пикселей. Но сразу бросается в глаза гигантский вес этого изображения — 2,5 МБ! Окей, наверняка там наш любимый растр в векторном формате. Давайте посмотрим — и мы оказались правы.
Но всё равно, даже для неоптимизированного PNG это очень большой размер, так что давайте раскодируем и посмотрим, что там дальше. А дальше у нас оказывается огромный растровый спрайт.

Поэтому я хочу напомнить, что желательно всегда проверять, что у нас действительно векторное изображение в формате SVG.
А что такое растровый PNG-спрайт и для чего он нужен?
Растровый PNG-спрайт — это одно изображение, содержащее несколько графических элементов (например, иконки), объединённых в единый файл PNG.
Зачем он нужен?
Уменьшает количество HTTP-запросов (ускоряет загрузку сайта).
Экономит трафик за счёт общего сжатия.
Позволяет управлять элементами с помощью CSS (background-position).
Как работает?
В CSS задаётся background-image: url(sprite.png). Для разных элементов смещается фон (background-position) так, чтобы отображалась нужная часть спрайта.
Когда использовать PNG-спрайты в 2026-м?
Если необходимо поддерживать старые браузеры (IE11 в корпоративных системах, устаревшие встроенные браузеры Smart TV) — они могут плохо работать с SVG и WebP. PNG имеет почти 100% поддержку, что делает его надёжным выбором.
Для сложных фотореалистичных иконок: SVG в силу своих ограничений могут не справляться с такими изображениями.
Для анимации на основе фреймов (sprite animation) и некоторых 2D-играх PNG остаётся универсальным форматом, если важна поддержка абсолютно всех устройств и программ.

Вся графика для сапера весит 2кб!
По итогу, чтобы использовать современные форматы, в идеальном мире нам придётся писать огромную портянку кода. В примере указано адаптивное изображение, с помощью атрибута <media>, всего для двух брейкпоинтов, а их может быть 3 или 4.
<picture> <source type=”image/avif” media=”(max-width: 600px)” srcset=” images/mobile-image@1x.avif 1x, images/mobile-image@2x.avif 2x” > <source type=”image/webp” media=”(max-width: 600px)” srcset=” images/mobile-image@1x.webp 1x, images/mobile-image@2x.webp 2x” > <source media=”(max-width: 600px)” srcset=” images/mobile-image@1x.jpg 1x, images/mobile-image@2x.jpg 2x” > <source type=”image/avif” media=”(min-width: 601px)” srcset=” images/desktop-image@1x.avif 1x, images/desktop-image@2x.avif 2x” > <source type=”image/webp” media=”(min-width: 601px)” srcset=” images/desktop-image@1x.webp 1x, images/desktop-image@2x.webp 2x” > <source media=”(min-width: 601px)” srcset=” images/desktop-image@1x.jpg 1x, images/desktop-image@2x.jpg 2x” > <img src=”images/desktop-image@1x.jpg” alt=”Alt”> </picture>
Но можно воспользоваться компонентным подходом
Покажу на простом примере реализации компонента Picture на React, но это без проблем можно сделать на любом другом современном фреймворке: Angular, Vue или Astro.
Сперва опишем типизацию и дефолтные пропсы. Из обязательных пропсов у нас name — название изображения, атрибут alt и флаг isPng, от значения которого мы будем определять, делать нам фоллбэк на PNG или JPEG.
Из дефолтных пропсов указываем дефолтные вьюпорты, которые можно изменить при вызове компонента; также указываем значение атрибута loading="lazy", что положительно влияет на UX и отрисовку страницы. По умолчанию будем считать, что наши изображения адаптированы под ретина и обычные дисплеи.
Затем в компоненте генерируются srcSet и sizes, добавляются постфиксы и отдаётся итоговый JSX.
import React from "react"; type Viewport = { width: number; size: string; value: "s" | "m" | "l"; }; type PictureProps = { name: string; alt: string; responsive?: boolean; lazy?: boolean; className?: string; fallbackToPng?: boolean; viewports?: Viewport[]; retina?: boolean; }; const defaultViewports: Viewport[] = [ { width: 480, size: "(max-width: 600px)", value: "s" }, { width: 800, size: "(max-width: 1200px)", value: "m" }, { width: 1200, size: "1200px", value: "l" }, ]; const defaultProps: Partial<PictureProps> = { responsive: false, lazy: true, fallbackToPng: false, viewports: defaultViewports, retina: true, }; const Picture: React.FC<PictureProps> = (props) => { const { name, alt, responsive, lazy, className, fallbackToPng, viewports, retina } = { ...defaultProps, ...props, }; const basePath = /images/${name}; const fallbackFormat = fallbackToPng ? "png" : "jpg"; const generateSrcSet = (format: string): string => { if (!responsive) { const defaultValue = viewports![0].value; return ${basePath}@1x-${defaultValue}.${format}; } return viewports! .map((vp) => { const src1x = ${basePath}@1x-${vp.value}.${format} ${vp.width}w; const src2x = retina ? , ${basePath}@2x-${vp.value}.${format} ${vp.width * 2}w : ""; return ${src1x}${src2x}; }) .join(", "); }; const generateSizes = (): string | undefined => responsive ? viewports!.map((vp) => vp.size).join(", ") : undefined; return ( <picture> <source srcSet={generateSrcSet("avif")} type="image/avif" sizes={generateSizes()} /> <source srcSet={generateSrcSet("webp")} type="image/webp" sizes={generateSizes()} /> <img src={${basePath}@1x-${viewports![0].value}.${fallbackFormat}} srcSet={generateSrcSet(fallbackFormat)} sizes={generateSizes()} alt={alt} className={className} loading={lazy ? "lazy" : "eager"} /> </picture> ); }; export default Picture; <Picture name="example" alt="image-description" fallbackToPng={true} />
Важно указывать атрибуты width и height для тега <img> (или задавать их через CSS). Это резервирует место под изображение на странице ещё до его загрузки и предотвращает неприятные скачки контента (Cumulative Layout Shift, CLS), которые ухудшают пользовательский опыт и влияют на Core Web Vitals.
Но компонент не решает проблему конвертации — нам всё равно нужно вручную конвертировать множество изображений. И для помощи в автоматизации конвертирования нам приходит Sharp.
Sharp — это быстрая библиотека для обработки изображений в Node.js, использующая libvips. Это словно швейцарский нож для изображений, который умеет:
Сжимать, оптимизировать и конвертировать изображения в другие форматы.
Изменять размеры и обрезать.
Применять фильтры или добавлять ватермарки.
Можно использовать на сервере или со сборщиком типа webpack или Vite, а можно использовать как отдельное приложение.
Вот пример CLI-приложения на Node.js, где мы должны отдать приложению изображения двукратного размера, а оно автоматически создаст версии WebP, AVIF, уменьшит изображения для обычных дисплеев, добавит постфиксы, а ещё сделает JPEG прогрессивным.
import fs from 'fs/promises'; import path from 'path'; import sharp from 'sharp'; const sourceDir = './source'; const outputDir = './output'; async function clearOutputDir() { try { await fs.rm(outputDir, {recursive: true, force: true}); } catch (error) { console.error('Ошибка при очистке папки output:', error); } } async function getImageWidth(filePath) { const metadata = await sharp(filePath).metadata(); return metadata.width; } async function optimizeImages() { try { await clearOutputDir(); await fs.mkdir(outputDir, {recursive: true}); const files = await fs.readdir(sourceDir); for (const file of files) { const filePath = path.join(sourceDir, file); const extension = path.extname(file).toLowerCase(); if (!['.jpg', '.jpeg', '.png'].includes(extension)) continue; const fileName = path.basename(file, extension); const outputFilePath1x = path.join(outputDir, ${fileName}@1x${extension}); const outputFilePath2x = path.join(outputDir, ${fileName}@2x${extension}); await sharp(filePath) .resize({width: Math.round(await getImageWidth(filePath) / 2)}) .toFormat(extension === '.jpg' || extension === '.jpeg' ? 'jpeg' : extension.slice(1), { progressive: extension === '.jpg' || extension === '.jpeg', quality: 80 }) .toFile(outputFilePath1x); await sharp(filePath) .toFormat(extension === '.jpg' || extension === '.jpeg' ? 'jpeg' : extension.slice(1), { progressive: extension === '.jpg' || extension === '.jpeg', quality: 80 }) .toFile(outputFilePath2x); await sharp(filePath) .resize({width: Math.round(await getImageWidth(filePath) / 2)}) .toFormat('webp', { quality: 80 }) .toFile(path.join(outputDir, ${fileName}@1x.webp)); await sharp(filePath) .toFormat('webp', { quality: 80 }) .toFile(path.join(outputDir, ${fileName}@2x.webp)); await sharp(filePath) .resize({width: Math.round(await getImageWidth(filePath) / 2)}) .toFormat('avif', { quality: 50 }) .toFile(path.join(outputDir, ${fileName}@1x.avif)); await sharp(filePath) .toFormat('avif', { quality: 50 }) .toFile(path.join(outputDir, ${fileName}@2x.avif)); } console.log('🏁'); } catch (error) { console.error('Ошибка при обработке изображений:', error); } } optimizeImages();
Для оптимизации векторной графики в формате SVG можно использовать библиотеку SVGO. Её также можно подключить к сборщику либо использовать интерактивную онлайн-версию. SVGO делает следующее:
Удаляет ненужные атрибуты, пустые контейнеры, ненужные transform и метаданные.
Сжимает стили и цвета.
Оптимизирует пути <path>, убирает пустые строки, символы форматирования, неиспользуемые глифы и символы.

Image Proxy
До этого мы рассматривали инструменты, которые зачастую используются локально разработчиками. А что если у нас за графику отвечают контент-менеджеры? Тут мы поговорим про инфраструктурное решение на базе Image Proxy.
Image Proxy — это промежуточный сервер, который обрабатывает, оптимизирует и доставляет изображения пользователю.
Не все Image Proxy используют кеширование, но большинство прокси-серверов для обработки изображений имеют встроенную поддержку кеша для повышения производительности.
Можно выделить три основные варианта работы Image Proxy:
on-the-fly processing — обработка «на лету» при каждом запросе.
С кешированием на CDN — после первой обработки результат сохраняется в CDN.
Гибридный подход с TTL (время жизни кэша).
Из недостатков:
Дополнительная нагрузка на сервер, нужно больше ресурсов для хранения.
Задержка при первом запросе (изображение загружается и обрабатывается в реальном времени).
Как работает Image Proxy:
Вы загружаете изображения в любое место — на S3 или свой сервер.
Image Proxy получает к ним доступ и создаёт уникальный URL.
При первом запросе Image Proxy обрабатывает изображение «на лету».
Результат кэшируется и раздаётся через CDN.
Как Image Proxy понимает, какое изображение нужно отдать?
Есть несколько способов, как сервер понимает, какое изображение отдать пользователю. Рассмотрим один основной, где прокси-сервер анализирует заголовки запроса. Браузер всегда отправляет HTTP-заголовки при каждом запросе, включая запросы на HTML, CSS, JavaScript, изображения и другие ресурсы. Proxy или CDN анализирует заголовок Accept от браузера. Этот заголовок содержит информацию о том, какие форматы изображений поддерживаются устройством пользователя.
Accept: image/avif,image/webp,image/jpeg User-Agent: iPhone; Safari
Прокси определяет, что браузер поддерживает AVIF, и отправляет лучший формат. И тогда код для изображения вновь принимает привычный нам вид:
<img src="https://imageproxy.com/?url=https://example.com/image.jpg" alt="Описание">
Но если мы хотим отдавать изображения в зависимости от DPR, то без атрибута srcset всё равно не обойтись.
Вывод
Оптимизация изображений — это баланс между качеством, производительностью и поддержкой
Снижение общего веса страницы даже на 200–300 КБ может улучшить скорость загрузки на 10–20%. Это напрямую повышает метрики Core Web Vitals, такие как Largest Contentful Paint (LCP).
Экономия трафика особенно важна для мобильных пользователей и снижает нагрузку на сервер.
Гибкость и адаптивность: использование srcset, <picture> и современных форматов позволяет подстроиться под любые устройства.
Игнорирование этих практик ведёт к потере пользователей, снижению позиций в поиске и избыточной нагрузке на сервер. Оптимизированная графика = быстрый, удобный и конкурентоспособный сайт.
Читайте также:

