
Коротко о главномРазвернутьСвернуть
- Картинки обычно оказываются самой тяжёлой частью страницы, и почти всегда они тяжелее, чем нужно.
- Скриншот интерфейса на 2,1 МБ ужимается до 102 КБ без заметной глазу разницы, а фотография на 4,7 МБ теряет три четверти веса ещё до того, как вы дотронетесь до качества сжатия.
- Разница между четырёхсекундной загрузкой и загрузкой меньше секунды обычно лежит именно здесь, а не в бандле скриптов.
Картинки обычно оказываются самой тяжёлой частью страницы, и почти всегда они тяжелее, чем нужно. Скриншот интерфейса на 2,1 МБ ужимается до 102 КБ без заметной глазу разницы, а фотография на 4,7 МБ теряет три четверти веса ещё до того, как вы дотронетесь до качества сжатия.
Разница между четырёхсекундной загрузкой и загрузкой меньше секунды обычно лежит именно здесь, а не в бандле скриптов. Ниже пошаговая оптимизация изображений: что делать первым, какой формат выбирать под какой тип картинки и почему кодировать современные форматы нужно на сборке, а не на лету.
Шаг, который экономит больше всех остальных
Прежде чем подбирать кодек и качество, посмотрите на фактические размеры. Фотография 6000×4000 в карточке шириной 1200 CSS-пикселей отдаёт браузеру в двадцать пять раз больше пикселей, чем нужно обычному экрану, и вшестеро больше, чем нужно экрану с двойной плотностью.
В замерах автора чеклиста одно только уменьшение до реально отображаемого размера ужало тестовое изображение с 4,7 МБ до 1,1 МБ . Это минус 77% при нулевой потере качества: пиксели, которых не видно, просто перестали передаваться.
Как определить нужный размер: Отправная точка это ширина контейнера в CSS-пикселях. На обычном экране она же и есть нужная ширина в физических пикселях, на плотных экранах нужно кратно больше: у части ноутбуков это двойка, у большинства современных смартфонов тройка. Поэтому правильный инструмент здесь не фиксированный множитель, а атрибут srcset с несколькими вариантами файла: браузер сам возьмёт тот, что соответствует плотности экрана посетителя.
Формат под тип изображения, а не один на всё
Самая частая ошибка — выбрать один формат и применять его ко всему подряд. Между фотографией с плавными градиентами и скриншотом с текстом и резкими границами разница принципиальная, и сжимаются они противоположными способами.
- Фотографии — WebP (на 30–50% меньше JPEG) или AVIF (ещё примерно на 30% меньше WebP).
- Скриншоты и элементы интерфейса — PNG или WebP без потерь.
- Логотипы и иконки — SVG: вектор весит копейки и масштабируется без предела.
- Анимации — анимированный WebP вместо GIF, это экономит 60–80% веса.
Отдельно про текст. Сжатие с потерями на буквах и резких границах создаёт заметные ореолы вокруг символов, поэтому скриншоты и логотипы обрабатываются только алгоритмами без потерь. Инструменты вроде oxipng ужимают такие файлы на 20–60%, не меняя при этом ни одного пикселя.
Конвертация и сжатие — это два разных действия
Перевод PNG в JPEG экономит место. Сжатие получившегося JPEG экономит ещё столько же, и про этот второй проход регулярно забывают. Для фотографий рабочий диапазон качества это 75–82%: от стопроцентного такая картинка визуально неотличима, а весит заметно меньше.
Метаданные, которые вы отдаёте бесплатно
В файле с камеры или из редактора лежат EXIF, координаты GPS, модель камеры и метки программы обработки. Для показа в браузере это бесполезный груз весом от 10 до 30 КБ на файл, за одним исключением: тег ориентации. Часть снимков с телефона хранит пиксели неповёрнутыми и полагается на него, поэтому поворот нужно применить к самим пикселям до того, как метаданные срежутся, иначе фотографии лягут набок. На каталоге из тысячи товарных снимков набегает до 30 МБ трафика ни за что, а координаты съёмки вдобавок могут оказаться данными, которые вы не собирались публиковать.
AVIF: что это и почему его нельзя кодировать на лету
AVIF (AV1 Image File Format) выпущен Alliance for Open Media в 2019 году и построен на внутрикадровом кодировании видеокодека AV1. По сути это инструменты сжатия одного кадра AV1, упакованные в самостоятельный контейнер для картинок.
Перед WebP у него два практических преимущества: выше степень сжатия при равном качестве и глубина цвета до 12 бит с поддержкой расширенного динамического диапазона, тогда как WebP ограничен восемью битами и обычным диапазоном. Достигается это за счёт заметно более богатого набора инструментов предсказания, унаследованного от видеокодека.
Сколько это в килобайтах
В сравнительном тесте брали пейзажный кадр 1920×1080 с градиентами неба, текстурой листвы и границами зданий, исходник в BMP весил 5,93 МБ . Целевое качество задавали по метрике структурного сходства: она сравнивает сжатую картинку с исходной, где единица означает полное совпадение, а 0,92 примерно соответствует порогу, за которым разницу перестают замечать глазом. Результаты:
PNG без потерь 4,82 МБ качество эталонное, для фото не годится
JPEG качество 85 420 КБ структурное сходство 0,921
WebP качество 80 298 КБ
AVIF 210 КБ вдвое меньше JPEG и на 29,5% меньше WebPНа листве и градиентах неба разница видна и глазом: JPEG на низком битрейте показывает цветовые блоки, у AVIF артефактов практически нет. Отвечают за это многоуровневые фильтры внутри цикла кодирования, которые заглаживают границы блоков и восстанавливают детали текстур.
Плата за это — время. Кодирование AVIF примерно в пятнадцать раз дольше JPEG и вчетверо дольше WebP, потому что выбор разбиения и типа преобразования требует перебора с оптимизацией. Зато декодирование медленнее JPEG всего вдвое, и на просмотр это почти не влияет. Практический вывод прямой: генерируйте AVIF заранее, на этапе сборки. Сервисы доставки изображений кодируют его и по запросу, но только с кешированием результата, чтобы платить за кодирование один раз на вариант, а не на каждого посетителя. Чего делать нельзя, так это кодировать заново при каждом обращении.
Как отдавать современный формат и не потерять старые браузеры
Поддержка AVIF в браузерах составляет примерно 96%, но подстраховка всё равно нужна. Делается она тегом <picture> : браузер берёт первый формат, который понимает, и до остальных не доходит.
<picture>
<source srcset="photo.avif" type="image/avif">
<source srcset="photo.webp" type="image/webp">
<img src="photo.jpg" alt="Описание изображения" width="1200" height="800" loading="lazy">
</picture>Атрибуты width и height здесь не декоративные: без них браузер не знает пропорций до загрузки файла и подвёрстывает страницу заново, когда картинка приходит. Это тот самый прыжок содержимого, который портит метрику визуальной стабильности. Атрибут loading=lazy убирает изображения за пределами первого экрана из очереди, конкурирующей за отрисовку главного элемента.
Именно за пределами: на картинку первого экрана его ставить нельзя. Отложенная загрузка задержит ровно тот элемент, скорость появления которого и меряет метрика отрисовки основного содержимого. Там уместен противоположный по смыслу fetchpriority=high .
Сжатие в браузере: почему это вообще возможно
Браузер умеет декодировать, преобразовывать и кодировать изображение прямо на устройстве, не отправляя файл никуда. Для товарных снимков, клиентских материалов и прочего чувствительного это снимает целый шаг из конвейера вместе с вопросом, где полежит копия.
Речь здесь уже не про ассеты вашего сайта, которые готовятся на сборке. Речь про картинки, которые в браузер приносит сам пользователь: форма загрузки в личном кабинете, объявление на маркетплейсе, вложение в заявку. Сжать их до отправки на сервер можно прямо во вкладке, и это снимает и трафик, и вопрос о том, где полежит оригинал.
Автор одного из таких инструментов разобрал устройство своего конвейера , и его опыт пригодится всем, кто выносит тяжёлые вычисления во фронтенд.
Вся тяжёлая работа уходит из главного потока
Перекодирование снимка 4000×4000 нагружает процессор настолько, что в главном потоке интерфейс замирает, а браузер показывает предупреждение о зависшей странице. Поэтому весь конвейер живёт в Web Worker: главный поток отвечает за выбор файлов, превью и состояние, а рабочий поток декодирует через createImageBitmap , применяет преобразования и кодирует через OffscreenCanvas . Именно через него, а не через canvas.toBlob : последний это метод DOM-элемента, а DOM в рабочем потоке отсутствует, и нужный метод там называется convertToBlob .
Важная деталь производительности: передавать данные между потоками нужно как ArrayBuffer через механизм передачи владения, а не структурным клонированием. На пачке из тридцати изображений это разница между мгновенным откликом и лишними полусекундами нагрузки на сборщик мусора для каждого файла.
Три ошибки, которые ищутся дольше всего
Рабочий поток не имеет доступа к window . Причём не только напрямую: достаточно подключить библиотеку, которая трогает window на верхнем уровне модуля. В консоли разработчика появляется ошибка обращения к неопределённому объекту, а вот в интерфейсе не появляется ничего: если сбой рабочего потока никак не показан пользователю, тот видит вечный спиннер. Лечится ревизией всех импортов рабочего потока: библиотеки, завязанные на браузерное окружение, остаются в главном потоке и передают в рабочий поток уже готовые данные.
Округление сломало масштабирование. Быстрый путь изменения размера использовал округлённый шаг по пикселям. На больших изображениях округление уводило исходную координату за пределы строки, и картинка выходила разрезанной по горизонтали с чёрной мозаикой внизу. Помогли точные дробные коэффициенты и ограничение по границам в каждом ручном проходе по пикселям.
Асинхронный обработчик сообщений перепутал порядок. Обработчик стал ждать преобразование уже после кодирования, а асинхронные обработчики порядок не сохраняют: состояние поворота разъехалось с готовым изображением. Полностью синхронным такой обработчик не сделать, декодирование и кодирование асинхронны по своей природе. Работает другое: обрабатывать сообщения строго по одному, не начиная следующее до завершения предыдущего, и хранить состояние вроде угла поворота рядом с данными конкретного сообщения, а не в общей переменной.
Заголовки безопасности ломают рабочие потоки молча: Блокирует загрузку скрипта заголовок Cross-Origin-Embedder-Policy: он требует, чтобы каждый сторонний ресурс отдавал разрешающий заголовок, иначе браузер молча его отбрасывает. Cross-Origin-Opener-Policy на это не влияет, но включают их обычно парой, поэтому и ломается всё в одном релизе. Любое изменение заголовков безопасности проверяйте на настоящем развёрнутом стенде, а не только на машине разработчика.
Что это даёт в цифрах
Скриншот 1920×1080 проходит такой путь: исходный PNG весит 2,1 МБ, после уменьшения и перевода в WebP с качеством 75% — 186 КБ, то есть минус 91%. Тот же кадр в AVIF занимает 102 КБ, минус 95% от исходного.
Именно эти проценты и отделяют страницу, которая грузится четыре секунды, от страницы, которая укладывается в одну.
- 01 Сверьте пиксели с вёрсткой Откройте панель сети и сравните натуральный размер картинок с размером контейнера. Если самый крупный вариант заметно превышает то, что нужно даже плотному экрану, уменьшайте до начала любых других работ.
- 02 Разделите картинки по типам Фотографии, скриншоты, логотипы и анимации обрабатываются разными алгоритмами. Один формат на весь проект гарантированно проигрывает и в весе, и в качестве.
- 03 Снимите метаданные Уберите EXIF и координаты при сборке. Это от 10 до 30 КБ на файл и заодно данные, которые вы вряд ли собирались публиковать.
- 04 Добавьте второй проход сжатия После конвертации прогоните файл через сжатие: для фотографий качество 75–82%, для текстовых изображений только алгоритмы без потерь.
- 05 Перенесите генерацию форматов в сборку AVIF кодируется примерно в пятнадцать раз дольше JPEG, поэтому его место в конвейере сборки, а не в обработчике запроса.
- 06 Пропишите размеры и отложенную загрузку Атрибуты ширины и высоты убирают прыжок вёрстки, а отложенная загрузка снимает конкуренцию за отрисовку первого экрана.
Уменьшение размера в пикселях до фактически отображаемого в вёрстке. Фотография 6000×4000 в карточке шириной 1200 пикселей отдаёт браузеру в двадцать пять раз больше пикселей, чем нужно обычному экрану. В замерах автора чеклиста один этот шаг убрал 77% веса, с 4,7 МБ до 1,1 МБ, при нулевой потере качества. Подбор кодека и качества имеет смысл только после него.
AVIF даёт файл вдвое меньше JPEG при равном качестве, поддерживает 12 бит и расширенный динамический диапазон, но кодируется примерно в пятнадцать раз дольше JPEG и вчетверо дольше WebP. Если изображения готовятся заранее, на сборке, берите AVIF с запасным WebP через тег picture. Если картинка создаётся в момент запроса, WebP практичнее: кодируется быстрее и меньше нагружает сервер.
На кадре 1920×1080 при структурном сходстве около 0,92 JPEG с качеством 85 занял 420 КБ, а AVIF — 210 КБ, то есть ровно вдвое меньше и на 29,5% меньше WebP. PNG без потерь на том же кадре весил 4,82 МБ и для фотографий не подходит.
Сжатие с потерями размывает резкие границы и создаёт заметные ореолы вокруг букв, из-за чего текст на скриншоте становится грязным и хуже читается. Для скриншотов, интерфейсов и логотипов применяют алгоритмы без потерь, например oxipng: они уменьшают файл на 20–60%, не меняя при этом ни одного пикселя изображения.
Да, нужны в любом случае. Атрибуты в разметке сообщают браузеру пропорции картинки ещё до загрузки файла, поэтому место под неё резервируется заранее. Без них страница подвёрстывается заново в момент загрузки изображения, и содержимое прыгает под курсором у читателя, а это прямо ухудшает метрику визуальной стабильности.
Что забрать с собой
Порядок действий важнее набора инструментов. Сначала пиксели, потом формат под тип изображения, потом второй проход сжатия, и только затем разговор про кодеки нового поколения. Обратный порядок даёт красивую строчку в отчёте и почти никакого эффекта на реальной странице.
Материалы разбора: чеклист оптимизации с замерами , техническое устройство AVIF и сравнительный тест форматов и разбор браузерного конвейера сжатия .
Откройте свою главную страницу с открытой панелью сети и отсортируйте запросы по размеру. Первые три строки почти наверняка окажутся картинками, и это ваша ближайшая оптимизация изображений: уменьшить их вдвое реально за один вечер.


