Изменение размера изображений на лету
Одной из многих проблем веб-архитектора является управление активами. И самая важная проблема в ресурсах — это изображения. Наивным подходом было бы установить изображение и позволить браузеру изменять его размер с помощью CSS:
img {
height: 100%;
width: 100%;
object-fit: contain;
}
Однако это означает, что вы загружаете исходное изображение. Это влечет за собой две проблемы: размер исходного изображения и неоптимальное изменение размера в браузере.
В этом посте будут рассмотрены две альтернативы: традиционные и совершенно новые решения.
Предварительное изменение размера
Традиционным решением для единого источника изображения является предварительное изменение размера. Перед выпуском дизайнерам потребовалось время, чтобы предоставить несколько версий изображений в разных разрешениях. В этом блоге я использую эту технику. Я предоставляю три разрешения для отображения основного изображения поста в разных контекстах в качестве фонового изображения:
- большое для поста на его странице
- среднее для публикации на главной странице
- маленькое для связанных сообщений на странице сообщения
Я также удаляю метаданные JPEG для еще большего уменьшения размера.
Тем не менее, традиционный подход заключается в использовании тега picture
HTML:
HTML-элемент<picture>
содержит ноль или более элементов<source>
и один элемент<img>
, позволяющий предлагать альтернативные версии изображения для различных сценариев отображения/устройства.
Браузер рассмотрит каждый дочерний элемент<source>
и выберет среди них наилучшее соответствие. Если совпадений не обнаружено (или браузер не поддерживает элемент<picture>
), выбирается URL-адрес атрибута src элемента<img>
. Выбранное изображение затем отображается в пространстве, занимаемом элементом<img>
.
Элемент Picture в веб-документах MDN
В свою очередь, его можно использовать следующим образом:
<picture>
<source media="(max-width: 199px)" srcset="ai-generated-200.jpg" />
<source media="(max-width: 399px)" srcset="ai-generated-400.jpg" />
<source media="(max-width: 599px)" srcset="ai-generated-600.jpg" />
<source media="(max-width: 799px)" srcset="ai-generated-800.jpg" />
<source media="(max-width: 999px)" srcset="ai-generated-1000.jpg" />
<img src="ai-generated.jpg" />
</picture>
Этот способ работал целую вечность, но у него есть две проблемы. Во-первых, предоставление нескольких разрешений для каждого изображения занимает много времени. С помощью ИИ можно автоматизировать процесс и получить хорошие результаты.
Однако объем необходимого хранилища может быть в два или три раза больше размера исходного изображения, в зависимости от количества созданных дополнительных разрешений. Например, в среде с богатыми активами электронная коммерция значительно увеличит затраты.
Изменение размера на лету
Недавно я наткнулся на imgproxy
, компонент для оперативного изменения размера изображений:
imgproxy
ускоряет работу веб-сайтов и приложений, одновременно экономя место на хранилище и затраты на SaaS
Он предлагает конечную точку, куда вы можете отправить закодированный URL-адрес, который определяет:
- Изображение, размер которого нужно изменить, и его местоположение, например, локальное, URL-адрес HTTP, сегмент S3 и т. д.
- Различные параметры размеров, например, размеры, подходят ли они или заполняются и т. д.
- Формат
imgproxy
поддерживает стандартные форматы, такие как JPEG и PNG, а также более современные, такие как WebP и AVIF. Он также может выбрать лучший формат в зависимости от заголовкаAccept
. - Множество (много!) других опций, таких как водяные знаки, фильтрация, поворот и т. д.
imgproxy
предлагает как бесплатную, так и платную версию с открытым исходным кодом.
Одним из решений было бы, чтобы веб-разработчик закодировал каждый URL-адрес imgproxy
в HTML:
<picture>
<source media="(max-width: 199px)" srcset="http://imgproxy:8080//rs:fill/w:200/plain/http://server:3000/ai-generated.jpg@webp" />
<source media="(max-width: 399px)" srcset="http://imgproxy:8080//rs:fill/w:400/plain/http://server:3000/ai-generated.jpg@webp" />
<source media="(max-width: 599px)" srcset="http://imgproxy:8080//rs:fill/w:600/plain/http://server:3000/ai-generated.jpg@webp" />
<source media="(max-width: 799px)" srcset="http://imgproxy:8080//rs:fill/w:800/plain/http://server:3000/ai-generated.jpg@webp" />
<source media="(max-width: 999px)" srcset="http://imgproxy:8080//rs:fill/w:1000/plain/http://server:3000/ai-generated.jpg@webp" />
<img src="ai-generated.jpg" />
</picture>
На веб-странице происходит утечка информации, связанной с топологией. Это необслуживаемое решение. Мы можем решить проблему с помощью обратного прокси-сервера или шлюза API. Я буду использовать Apache APISIX по понятным причинам.
При таком подходе приведенный выше HTML становится намного проще:
<picture>
<source media="(max-width: 199px)" srcset="/resize/200/ai-generated.jpg" />
<source media="(max-width: 399px)" srcset="/resize/400/ai-generated.jpg" />
<source media="(max-width: 599px)" srcset="/resize/600/ai-generated.jpg" />
<source media="(max-width: 799px)" srcset="/resize/800/ai-generated.jpg" />
<source media="(max-width: 999px)" srcset="/resize/1000/ai-generated.jpg" />
<img src="ai-generated.jpg" />
</picture>
Apache APISIX перехватывает запросы, начинающиеся с /resize
, перезаписывает URL-адрес для imgproxy
и перенаправляет переписанный URL-адрес в imgproxy
. Вот общий поток:
Соответствующая конфигурация Apache APISIX выглядит следующим образом:
routes:
- uri: /resize/*
plugins:
proxy-rewrite:
regex_uri:
- /resize/(.*)/(.*)
- /rs:fill/w:$1/plain/http://server:3000/$2@webp
upstream:
nodes:
"imgproxy:8080": 1
- Запросы на совпадение с префиксом /resize
- Перепишите URL-адрес
- Улавливает ширину и изображение в регулярном выражении
- Отформатирует URL-адрес
imgproxy
.http://server:3000
— сервер, на котором размещено исходное изображение;@webp
указывает на предпочтение формата WebP (если браузер его поддерживает)
С учетом вышеизложенного /resize/200/ai-generated.jpg
для Apache APISIX перезаписывается как /rs:fill/w:200/plain/http://server:3000/ai-generated.jpg@webp
в imgproxy
.
Тестирование
Мы можем настроить небольшой тестовый образец с помощью Docker Compose:
services:
apisix:
image: apache/apisix:3.5.0-debian
volumes:
- ./apisix/config.yml:/usr/local/apisix/conf/config.yaml:ro
- ./apisix/apisix.yml:/usr/local/apisix/conf/apisix.yaml:ro
ports:
- "9080:9080"
imgproxy:
image: darthsim/imgproxy:v3.19
server:
build: content
Простой веб-сервер, на котором размещается HTML и основное изображение.
Теперь мы можем протестировать описанную выше настройку с помощью инструментов разработчика браузера, эмулируя устройства с небольшим экраном, например iPhone SE. Результат следующий:
- Из-за разрешения экрана запрошенное изображение имеет ширину 400 пикселей, а не исходное. Вы можете увидеть это в URL-адресе запроса.
- Возвращенное изображение имеет формат WebP; его вес 14,4кб
- Исходное изображение в формате JPEG весит 154 КБ, что более чем в десять раз больше. Это существенная экономия пропускной способности сети!
Обсуждение
Сокращение затрат на хранение в десять раз, естественно, является большим преимуществом. Однако это не только единороги и радуги. Вычисление изображения с измененным размером — это трудоемкая операция; на каждый запрос тратится процессорное время. Более того, какой бы эффективной imgproxy
она ни была, на создание имиджа требуется время. Мы заменили затраты на хранилище на затраты на процессор, и теперь мы столкнулись с небольшим снижением производительности.
Чтобы это исправить, нам нужен слой кэширования спереди, либо собственный, либо, что более вероятно, CDN. Вы можете возразить, что мы снова будем хранить активы; таким образом, затраты на хранение снова вырастут. Однако существенное отличие состоит в том, что кеш работает только для использованных изображений, тогда как в первом решении мы ранее платили за хранение всех изображений. Вы также можете применить известные рецепты кэширования, такие как предварительное прогревание, когда вы знаете, что группа изображений будет пользоваться большим спросом, например, перед каким-либо событием.
Заключение
В этом посте мы описали, как использовать Apache APISIX imgproxy
для снижения стоимости хранения изображений в разных разрешениях. Благодаря кэшированию на вершине в общую архитектуру добавляется больше компонентов, но при этом сокращаются затраты на хранение.