Реклама
Перетяжка // Коробка 3.0

Оптимизация изображений для веба: где теряются мегабайты

Скриншот на 2,1 МБ ужимается до 102 КБ, а фотография теряет три четверти веса ещё до разговора о качестве сжатия. Разбираем порядок действий, при котором картинки перестают быть самой тяжёлой частью страницы.

Обложка: Оптимизация изображений для веба: где теряются мегабайты

Картинки обычно оказываются самой тяжёлой частью страницы, и почти всегда они тяжелее, чем нужно. Скриншот интерфейса на 2,1 МБ ужимается до 102 КБ без заметной глазу разницы, а фотография на 4,7 МБ теряет три четверти веса ещё до того, как вы дотронетесь до качества сжатия.

Разница между четырёхсекундной загрузкой и загрузкой меньше секунды обычно лежит именно здесь, а не в бандле скриптов. Ниже пошаговая оптимизация изображений: что делать первым, какой формат выбирать под какой тип картинки и почему кодировать современные форматы нужно на сборке, а не на лету.

Ключевые выводы

Первым делом уменьшайте размер в пикселях, а не качество: фотография 6000×4000 в карточке шириной 1200 пикселей это чистые потери. Один этот шаг убрал 77% веса тестового снимка.

Формат выбирается под тип картинки: фотографиям WebP или AVIF, скриншотам и интерфейсам сжатие без потерь, логотипам SVG, анимациям анимированный WebP вместо GIF.

AVIF при равном качестве весит вдвое меньше JPEG, но кодируется примерно в пятнадцать раз дольше, поэтому генерировать его нужно на этапе сборки.

Метаданные съёмки занимают от 10 до 30 КБ на файл и в вебе не нужны ни для чего.

Сжатие с потерями на тексте и резких границах даёт видимые артефакты: скриншоты и логотипы жмутся только без потерь, зато на 20–60%.

Шаг, который экономит больше всех остальных

Прежде чем подбирать кодек и качество, посмотрите на фактические размеры. Фотография 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% от исходного.

Именно эти проценты и отделяют страницу, которая грузится четыре секунды, от страницы, которая укладывается в одну.

Чеклист: пройтись по своим картинкам
  1. 01
    Сверьте пиксели с вёрсткой

    Откройте панель сети и сравните натуральный размер картинок с размером контейнера. Если самый крупный вариант заметно превышает то, что нужно даже плотному экрану, уменьшайте до начала любых других работ.

  2. 02
    Разделите картинки по типам

    Фотографии, скриншоты, логотипы и анимации обрабатываются разными алгоритмами. Один формат на весь проект гарантированно проигрывает и в весе, и в качестве.

  3. 03
    Снимите метаданные

    Уберите EXIF и координаты при сборке. Это от 10 до 30 КБ на файл и заодно данные, которые вы вряд ли собирались публиковать.

  4. 04
    Добавьте второй проход сжатия

    После конвертации прогоните файл через сжатие: для фотографий качество 75–82%, для текстовых изображений только алгоритмы без потерь.

  5. 05
    Перенесите генерацию форматов в сборку

    AVIF кодируется примерно в пятнадцать раз дольше JPEG, поэтому его место в конвейере сборки, а не в обработчике запроса.

  6. 06
    Пропишите размеры и отложенную загрузку

    Атрибуты ширины и высоты убирают прыжок вёрстки, а отложенная загрузка снимает конкуренцию за отрисовку первого экрана.

Часто задаваемые вопросы
1
Что даёт больше всего экономии при оптимизации картинок?

Уменьшение размера в пикселях до фактически отображаемого в вёрстке. Фотография 6000×4000 в карточке шириной 1200 пикселей отдаёт браузеру в двадцать пять раз больше пикселей, чем нужно обычному экрану. В замерах автора чеклиста один этот шаг убрал 77% веса, с 4,7 МБ до 1,1 МБ, при нулевой потере качества. Подбор кодека и качества имеет смысл только после него.

2
AVIF или WebP: что выбрать?

AVIF даёт файл вдвое меньше JPEG при равном качестве, поддерживает 12 бит и расширенный динамический диапазон, но кодируется примерно в пятнадцать раз дольше JPEG и вчетверо дольше WebP. Если изображения готовятся заранее, на сборке, берите AVIF с запасным WebP через тег picture. Если картинка создаётся в момент запроса, WebP практичнее: кодируется быстрее и меньше нагружает сервер.

3
Насколько AVIF меньше JPEG в реальном тесте?

На кадре 1920×1080 при структурном сходстве около 0,92 JPEG с качеством 85 занял 420 КБ, а AVIF — 210 КБ, то есть ровно вдвое меньше и на 29,5% меньше WebP. PNG без потерь на том же кадре весил 4,82 МБ и для фотографий не подходит.

4
Почему нельзя сжимать скриншоты как фотографии?

Сжатие с потерями размывает резкие границы и создаёт заметные ореолы вокруг букв, из-за чего текст на скриншоте становится грязным и хуже читается. Для скриншотов, интерфейсов и логотипов применяют алгоритмы без потерь, например oxipng: они уменьшают файл на 20–60%, не меняя при этом ни одного пикселя изображения.

5
Нужны ли атрибуты width и height, если размеры заданы в CSS?

Да, нужны в любом случае. Атрибуты в разметке сообщают браузеру пропорции картинки ещё до загрузки файла, поэтому место под неё резервируется заранее. Без них страница подвёрстывается заново в момент загрузки изображения, и содержимое прыгает под курсором у читателя, а это прямо ухудшает метрику визуальной стабильности.

Что забрать с собой

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

Материалы разбора: чеклист оптимизации с замерами, техническое устройство AVIF и сравнительный тест форматов и разбор браузерного конвейера сжатия.

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