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

С увеличением разрешения экранов, усложнением дизайна и тенденцией к использованию изображений высокого качества нагрузка на сайты продолжит расти. Это напрямую влияет на скорость загрузки, потребление трафика и, в конечном итоге, на пользовательский опыт.

Просматривая недавно пул-реквест, я заметил, что туда добавили новые изображения. Меня удивил выбор формата: для картинок без прозрачности использовали PNG. Это кажется избыточным, так как для многоцветных изображений лучше подходят форматы вроде JPEG, которые весят меньше.

Я решил провести эксперимент: оптимизировал и переконвертировал изображения в JPEG, WebP и AVIF, сохранив качество на глаз неотличимым от оригинала.

WebP, AVIF, JPEG и исходный PNG
WebP, AVIF, JPEG и исходный PNG

Результат оказался впечатляющим: размер файлов 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-пиксели, но разработчикам важно учитывать это при создании графики и адаптивного дизайна.

DPR
DPR

«Ретина» — маркетинговое название 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кб!
    Вся графика для сапера весит 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:

  1. on-the-fly processing — обработка «на лету» при каждом запросе.

  2. С кешированием на CDN — после первой обработки результат сохраняется в CDN.

  3. Гибридный подход с TTL (время жизни кэша).

Из недостатков:

  • Дополнительная нагрузка на сервер, нужно больше ресурсов для хранения.

  • Задержка при первом запросе (изображение загружается и обрабатывается в реальном времени).

Как работает Image Proxy:

  1. Вы загружаете изображения в любое место — на S3 или свой сервер.

  2. Image Proxy получает к ним доступ и создаёт уникальный URL.

  3. При первом запросе Image Proxy обрабатывает изображение «на лету».

  4. Результат кэшируется и раздаётся через 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> и современных форматов позволяет подстроиться под любые устройства.

  • Игнорирование этих практик ведёт к потере пользователей, снижению позиций в поиске и избыточной нагрузке на сервер. Оптимизированная графика = быстрый, удобный и конкурентоспособный сайт.


Читайте также:

BYOVD-атаки на ядро Windows через драйверы: разбираю механику, воспроизвожу, строю защиту
Вы настроили Sysmon, у вас работает EDR, события летят в SIEM. Создаётся процесс, вы видите Event ID...
habr.com
Фронтенд 2026: что умерло, что выжило и что взлетело неожиданно
Я открываю старый проект 2020 года и вижу знакомые имена в package.json: create-react-app, enzyme, m...
habr.com