Pull to refresh
1726.56
МТС
Про жизнь и развитие в IT

Как мы уменьшаем размер изображений на веб-страницах в 10 раз с помощью нашего оптимизатора

Reading time8 min
Views19K

Привет, Хабр! Меня зовут Евгений Лабутин, я из команды разработки продукта МТС Твой бизнес. Мы разработали свой рецепт приготовления картинок для нашего портала. Благодаря ему удалось сократить их вес на странице до 10 раз относительно уже оптимизированного jpg/png, сохранив при этом простоту разработки – как будто это стандартный img элемент.

Разработанный микросервис называется ImageOptimize, из этой статьи вы узнаете, как он работает и что у него под капотом. Мы уже выложили код микросервиса в OpenSource (чему очень рады), поэтому вы тоже можете использовать такую компрессию, настроив ее в несколько простых шагов.

Для чего уменьшается вес страниц

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

Картинки на веб-странице занимают основную массу в объеме загрузки данных. Например, на главной Хабра именно изображения – это 88% от веса всей страницы (на 03.02.2022 в FullHD разрешении). 6 мб из 6,8 мб занимают картинки. При этом большая часть информации на главной – это текст.

Такой дисбаланс возникает из-за того, что остальную часть программы составляют код и стили, для которых используются сборщики и оптимизаторы. А для картинок такие инструменты не применяются.

Для решения этой проблемы мы изготовили свой оптимизатор картинок. Как результат – на главной странице нашего продукта 63 изображения, и при этом она весит всего 2,4 мб, из которых картинки – только 754 кб.

В чем секрет уменьшения веса

Секрет на самом деле прост, для достижения результата мы используем два принципа:

  • Применяем современные форматы изображения, такие как webp и avif,

  • Используем тот размер картинки, который отображается в верстке пользователя.

Кроме того, вся оптимизация происходит на лету,  а это дает следующие преимущества:

  • Не нужно предварительно оптимизировать картинки, достаточно положить на сервер исходники

  • Избавляет от проблем, если пользователь выкладывает в блог неоптимизированные картинки на 15 мб

  • Нет необходимости пережимать все изображения – в случае выхода нового формата достаточно обновить зависимости и у пользователей картинки станут еще лучше

  • Клиентский компонент определяет размер изображения в верстке на стороне пользователя, после чего запрашивает именно этот размер на сервере

  • Тот же компонент определяет поддержку форматов avif и webp, поэтому новейшие браузеры получают avif, старые – webp, а совсем уж древние – jpg/png.

Все это работает очень быстро, на конвертацию FullHD картинки в формат webp уходит около 200 мс, в формат avif – 800 мс, а с учетом кеширования на прокси сервере и добавление кеширующих заголовков клиенты получают результат почти мгновенно.

Как все устроено

Решение работает на стыке технологий frontend и devops. Состоит оно из трех простых элементов, которые мы решили использовать вместе:

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

  2. Кеширующий прокси-сервер (nginx или аналог). Отвечает за кеширование результата работы микросервиса оптимизации картинок и добавление кеширующих заголовков

  3. Микросервис оптимизации. Выполнен в виде контейнера Docker. Именно он получает запрос от клиента, скачивает исходник картинки, пережимает его по параметрам, полученным от клиентского компонента и отправляет результат назад

Микросервис оптимизации

Репозиторий контейнера можно найти на GitHub, а готовый образ в репозитории DockerHub.

Внутри контейнера находится очень простая программа, написанная на nodejs. За прием и обработку запросов от клиента в ней отвечает фреймворк NestJS, а за конвертацию изображений – высокопроизводительная и многопоточная библиотека SharpJS.

Для разворачивания достаточно выполнить команду:

docker run -d --restart always -p 3000:3000 mtsrus/image-optimize

после чего можно проверить работу, открыв ссылку в браузере:

http://localhost:3000/optimize?size=1060&format=webp&src=https://tb.mts.ru/static/landing/images-index2/banner/slider/partners.png

Должно открыться изображение шириной 1060 пикселей в формате webp. Если поменять параметр size, то изображение откроется в другом размере. Если же параметр format поменять на avif или jpg, или png то картинка откроется в соответствующем формате, если src – то можно выбрать другое изображение для компрессии. Причем все эти конвертации происходят на лету за очень короткое время.

Как видите, src работает с любым http(s) источником, но именно ваш пул реквест может дополнить его функцией чтения картинки с диска или из post запроса!

В случае, если ваш контур закрыт базовой авторизацией, в переменные окружения контейнера можно добавить переменную BASIC_AUTHS в формате encodeURIComponent("url"):login:password для прохождения авторизации.

В целях безопасности рекомендуется ограничить доступные адреса с исходниками для предотвращения использования вашего микросервиса для оптимизаций чужих картинок, а также предотвращения скачивания локальных картинок из вашей сети. Для этого достаточно задать переменную окружения в контейнере ALLOW_SOURCES, например, значением http://localhost:3000/

Также контейнер оснащен экспортером Prometheus, при помощи которого вы сможете наблюдать внутреннее состояние системы.

Прокси-сервер

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

Для этого достаточно настроить nginx (или его аналог).

Пример конфига для nginx:

proxy_cache_path  /var/cache/nginx/cache levels=1:2 keys_zone=STATIC:50m max_size=3g inactive=30000m;

...

location /optimizer/ {

    # Информационноый заголовок для определения статуса кеша
    add_header X-Cache-Status $upstream_cache_status;

    # Кешируем на клиенте на 3 года, согласно рекомендаций Google Lighthouse
    expires 3y;

    # Настраиваем кеш на сервере на 1 сутки
    proxy_cache STATIC;
    proxy_cache_valid      200  1d;
    proxy_cache_use_stale  error timeout invalid_header updating http_500 http_502 http_503 http_504;

    # Если кеш протух, клиенту вернется старый, а сервер пойдет за новым. Клиент не будет ждать
    proxy_cache_background_update on;

    # Посылать только один запрос на микросервис оптимизации, остальные будут ждать в очереди
    proxy_cache_lock on;

    # Указываем адрес микросервиса оптимизации
    proxy_pass http://localhost:3000/;
}

# Блокируем доступ к экспортеру Prometheus если не нужен доступ из вне
location /optimizer/metrics {
    deny    all;
}

Теперь мы можем нагружать наш сервер миллионами однотипных запросов и он при этом не будет испытывать нагрузки.

После настройки серверной части самое время начать использовать наш микросервис на клиентской стороне.

Клиентский компонент

Для использования на клиенте написан специальный компонент под библиотеку React. Исходный код которого лежит на GitHub, а собранный пакет на npm.

Мы планируем сделать компонент на базе технологии Web Components для использования в любом другом фреймворке или даже в голой верстке.

Как использовать компонент? Очень просто:

import {Image} from "@mts-pjsc/image-optimize";

<Image
    alt="Sample of work Image Optimizer"
    src="/static/landing/images-getmeback/phone-fon.png"
/>

Как видите, работает так же просто, как и элемент img. Далее всю магию компонент сделает сам. Определит, поддерживает ли браузер avif, webp, jpg/png и размер изображение в верстке и запросит у микросервиса наиболее подходящую картинку.

Пример непосредственной работы этого компонента вы можете увидеть на странице нашего продукта.

Есть важный для понимания момент, связанный с механизмом измерения размера картинки. Для корректного вычисления ее ширина должна быть установлена изначально. Нужно задать стили width: 100% или width: 50vw или width: 550px. В противным случае у вас загрузится минимально возможная ширина картинка.

Вычисления происходят при помощи прозрачной картинки размером 1х1 пиксель. Поэтому она должна занять правильную ширину до момента измерения.

Таким образом мы получаем максимально оптимизированные картинки.

Если посмотреть на раздел "Что нового" на стартовой странице МТС Твой Бизнес, то на десктопе понадобится картинка в 144 пикселя, а на мобильном устройстве Samsung Galaxy S8+ – изображение 148 пикселя * 4 (масштабирование Device Pixel Ratio) = 592 пикселя по ширине. Потому что на этом устройстве разрешение экрана 1440x2960 и для качественного отображения контента нужны более качественные картинки. При этом на старых гаджетах понадобятся картинки размером 148 пикселей по ширине или даже меньше (в зависимости от разрешения экрана).

На этом примере можно хорошо заметить преимущества измерения размера картинки перед определением размера картинки по ширине экрана посредством стандартного элемента picture. Ведь картинка занимает всего 1/3 экрана, а значит, можно загрузить ее гораздо легче. В этом и состоит второй принцип оптимизации изображений.

У такого подхода есть и еще одно преимущество. В случае переверстки этого блока и изменения размера картинок, их не надо будет пережимать под новый размер, компонент в связке с микросервисом сделает все сам.

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

Простой и быстрый инструмент

Мы получили чрезвычайно простой и гибкий механизм оптимизации изображений для веб-страницы. Развернуть его можно буквально за 10 минут и сразу получить преимущества от быстро открывающихся страниц.

Что касается размера, то для примера можно взять оригинальную картинку, которая весит 39 КБ, что, казалось бы, оптимально для веба. Но если использовать микросервис, то она весит уже 2,6 КБ в современном chrome, то есть в 15 раз меньше, и это еще без оптимизаций по размеру. А вес всех картинок на главной странице, которая содержит 63 изображения, составляет всего 754 кб. В среднем уменьшение веса составляет примерно в 10 раз, в некоторых случаях – до 40 раз, а если брать случаи, когда пользователь выгружает исходники – то и вовсе в 1000 раз.

Но не все так гладко. Компонент написан с учетом наших потребностей и с учетом сложившихся практик. Поэтому мы загружаем картинки не того размера, что используется в верстке, а в одном из пяти размеров: 160, 320, 640, 1280, 1920 пикселей. Такие шаги позволяют нам гораздо эффективнее использовать кеш на стороне сервера.

Вопросы и ответы

В контейнере используется nodejs? Она же медленная и однопоточная?

NodeJS действительно однопоточная, но API за пределами движка V8 реализуется многопоточно. Библиотека SharpJS реализована как нативное API и без проблем нагружает все ядра процессора. Тем самым поток nodejs не блокируется во время конвертации изображений.

Но есть же гораздо более быстрые решения для обработки запросов чем nodejs?

Дело в том, что мы проповедуем концепцию Фронтенд Микросервисов. И если фронтендеру необходим микросервис – он сам его готовит на frontend технологиях (чтобы проще было поддерживать). Что касается скорости обработки запросов, то nodejs регулярно находится в топе бенчмарков и конкурирует с C++, C и Java. А с учетом многопоточной конверсии через SharpJS 99.9% нагрузки создает именно конвертация, а не обработка запросов. Поэтому оптимизация через отказ от nodejs в пользу C++ избыточна.

Что с нагрузкой? Какой RPS выдержит?

Все зависит от вашего железа. У нас одна FullHD картинка конвертируется порядка 200 мс на одном ядре в формате webp, или 5RPS на FullHD картинке на 1 ядре Xeon. Но по факту у нас на этот микросервис стоит ограничение в 4 ядра, и картинки в среднем в 4 раза меньше, из-за чего конвертируется в 8 раз быстрее. В итоге 5 RPS * 8 Облегчение * 4 Ядер = 160 RPS. После чего результат укладывается в кеш на сутки, за счет чего микросервис совсем перестает нагружаться и 99,9% времени простаивает (см. скриншот из Grafana выше). А из кеша можно раздавать хоть 1 000 000 RPS .

Чем это лучше чем у NextJS?

Во-первых, мы начали использовать эту оптимизацию за много лет до того, как она появилась в NextJS и даже сейчас она развивается гораздо быстрее, чем в NextJS. Во-вторых, не надо мучиться с параметром layout у компонента Image из Nextjs, наш компонент делает все сам. В-третьих, компонент определяет необходимый размер картинки в верстке, а не в экране пользователя. За счет этого происходит гораздо более корректный выбор. В-четвертых, вы можете использовать наш метод оптимизации в любом фреймворке, не только в NextJS.

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

Выражаю благодарность за иллюстрацию к статье главному дизайнеру проекта МТС Твой Бизнес, Экспертному центру по веб-разработке и Гильдии веб-разработчиков компании МТС за помощь в подготовке материала.

Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
Total votes 31: ↑29 and ↓2+33
Comments28

Articles

Information

Website
www.mts.ru
Registered
Founded
Employees
over 10,000 employees
Location
Россия