Назад в блог

Оптимизация изображений на крупных проектах

В этой статье мы бы хотели поделиться своим опытом обработки картинок на больших проектах. Там, где количество картинок переваливает за миллион, а архив всех картинок превышает 200ГБ и все это нужно ресайзить, накладывать водяные знаки и прочие эффекты.

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

На стандартных, не особо крупных проектах, превью картинок создаются в момент загрузки файла или при обращении к нему. Картинки кладутся в кэш и все последующие разы отдается уже из кэша.Звучит все правильно, подходит для большинства решений и это умеют делать почти все фреймворки и CMS на сегодняшний день.Так делаем и мы. В основе разработки мы используем фреймворк Laravel и для него есть решения, которые с этим отлично справляются.

Но при росте одного из проектов мы столкнулись со следующими проблемами:

  • При резком росте количества картинок уходит слишком много времени на их ресайз;
  • При большой посещаемости сайта PHP даже из кэша довольно долго отдает картинки;
  • В случае, если миниатюры создаются при загрузке изображений на сервер и сразу кладутся в кэш, то при появлении новых размеров картинок приходится заново ресайзить все изображения, что может затянуться на сутки, а то и более. Бизнес столько ждать не может;
  • Мы храним все размеры картинок для всех товаров, даже для тех, на которые, возможно, никто никогда и не зайдет или товар уже давно не в наличии. В итоге, неиспользуемые картинки продолжают храниться на диске, раздувая его объемы. Да, можно периодически удалять давно неиспользуемые картинки, но при больших объемах данных эта процедура может затянуться.

Ниже рассказываем о том, как мы решили эти проблемы. Заодно попутно удовлетворили google page speed test, который постоянно просит уменьшить размер картинок.

Выбор архитектуры

Сперва выбираем архитектуру, с помощью будем ресайзить и отдавать картинки.

Мы сравнили несколько способов и провели нагрузочное тестирование. С помощью утилиты wrk организовали нагрузку на сервер, который отдаёт картинки, в течении 10 секунд с 15 параллельными подключениями.

1. Самый популярный способ. Ресайзим картинки на PHP, PHP проверяет есть ли картинки в кэше и, если есть, отдает, если нет - ресайзит на лету.

В итоге 106 ответов в секунду, каждый из которых в среднем отдавался через 140 мс.

2. Пробуем ресайзить картинки в реальном времени с помощью одной из библиотек NodeJS.

Количество запросов и время отклика улучшилось почти в 3 раза: 283 ответа в секунду и каждый ответ в среднем 52мс.

3. К ресайзу картинок на NodeJS добавляем кэширование силами nginx. Т.е. nginx кладет к себе в кэш картинку и при последующем обращении отдает ее из кэша, не дергая NodeJS

Приведем результаты к сравнительной таблице:

Оптимальный вариант очевиден, выбираем последний способ.
php + cache на php nodejs в реальном времени nodejs + кэш на nginx
Среднее время ответа 140мс 52мс 7мс
Запросов в секунду 107 283 117000
Передано данных в секунду 22Кб 4.4Мб 1.6Гб

Описание технической стороны реализации

Nginx обрабатывает все запросы к картинкам. В случае, если на сервере уже есть миниатюра - отдаем из кэша nginx, если нет - запрос проксируется на nodejs. В настройках nginx это выглядит следующим образом:

                  proxy_cache_path /cache/imgproxy levels=1:2 keys_zone=imgproxy:50m max_size=50g inactive=30d use_temp_path=off;
server { location ~ ^/images/products/(?<id>\d+)/(?<type>.+)/(?<img>.+) { add_header X-Cache $upstream_cache_status; proxy_cache imgproxy; proxy_cache_valid 200 30d; proxy_pass http://127.0.0.1:1337/$type/products/$id/original/$img;
access_log off; allow all; expires 1M; add_header Cache-Control "public"; } }

Т.е. все запросы к картинкам по адресу /images/products/71481/211/0.jpg будут обрабатываться нашим механизмом. Где:

  • 71481 - Номер товара, чтобы nodejs понимал из какой директории брать оригинальное изображение.
  • 0 - Название оригинальной картинки, которую нам необходимо отресайзить, которая находится в директории товара 71481.
  • 211 - Название конфигурации параметров для ресайза картинок, которые мы храним в nodejs, к примеру в настройке 211 мы храним, что должна быть картинка 211 на 170 с качеством в 95%. Так, при появлении новых размеров картинок на сайте, мы просто добавляем новые, например small, large, 611x211, 611, как вам угодно, и всего лишь меняем путь к картинке. Это так же нас защищает, чтобы злоумышленники не создали кучу миниатюр, которые сайт на самом деле не использует, тем самым загрузив сервер.

В настройках nginx установлен максимальный размер кэша в 50Гб и срок хранения 30 дней. Часть, которая отвечает за ресайз картинок, реализованная на  NodeJS мы выложили на github: https://github.com/websecret/imgproxy.

Отметим, что сервер всегда отдает 200 ответ и указывает время истечения кэша, expires. Таким образом, при загрузке страницы, браузер клиента вообще не обращается к серверу для загрузки картинки, а сразу берет ее из кэша браузера. Ранее, в случае 304 ответа - этот ответа отдавался сервером, тем самым немного увеличивая время загрузки.

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

Мы будем использоваться https://github.com/lovell/sharp, так как она показала лучшие результаты тестов по скорости обработки картинок и конечному соотношению.

Для сравнения приводим пример двух картинок, которые ресайзились раньше с помощью Imagick, а теперь с помощью  https://github.com/lovell/sharp.

Imagick Sharp
Размер 20Кб Размер 8Кб
Размер 20Кб Размер 8Кб

Да, при использовании библиотеки Sharp мы уменьшили качество картинок со 100% до 95%, но для картинок такого размера разница не видна, а по размеру картинка разница очень существенная. В итоге размеры картинок уменьшились более, чем в два раза.

Для оценки результатов, как наши преобразования повлияли для посетителя сайта, сравним в консоли браузера (сперва старый способ, затем - наш новый новый механизм):

На этом все. Вдобавок, как писали в самом начале, мы сравниваем результаты GooglePageSpeed для страницы категории в интернет-магазине, где присутствует много миниатюр товаров. Тему, как достичь высоких показателей, мы сейчас поднимать не будем, лишь покажем как картинки влияют на этот тест.

Таким образом мы решили следующие проблемы и добились следующих результатов:

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

Надеюсь наш материал кому-то поможет решить схожие проблемы. Будем рады вопросам и предложениям.