Оптимизация изображений для web

    image

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

    Проблема


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

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

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

    В качестве примера, как делать не нужно можно посмотреть на главную страницу такого известного сайта, как github.com. При весе страницы 2 Мб, 1.2 из них занимают бесполезные картинки, которые можно оптимизировать и не загружать.

    image


    Второй пример — наш Хабр. Скриншот приводить не буду, что бы не растягивать статью, результаты по ссылке. На хабре картинкам изменяют разрешение на нужное, но не оптимизируют их. Это позволило бы сократить их размер на 650 Кб (50%).

    Во многих местах на сайте нужны уменьшенные версии картинок, например чтобы в ленте новостей показывать уменьшенную версию картинки новости. Мы это реализуем следующим образом— на нашем сервере хранится только картинка в максимальном качестве, а при необходимости вставить ее отресайженную версию надо дописать в конец урла требуемое разрешение через "@". Тогда запрос отправится не за файлом, а на наш ресайзящий бэкенд и вернет отресайженную и оптимизированную версию картинки.

    Распространенные решения


    Все, что будет сказано далее относится к JPEG и PNG изображениям, т.к. это наиболее популярные форматы в интернете.

    Вбив в google что-то вроде «image resize backend» вы увидите, что в половине случаев предлагается использовать Nginx, другая часть— это различные самописные сервисы, чаще всего Node.js.

    Из nginx, а точнее из libgd, которая используется в модуле nginx’а мы смогли выжать на тестовой картинке 63 RPS, что неплохо, но хотелось бы быстрее и больше гибкости. Graphicsmagick тоже не подходит, т.к. его скорость работы слишком низкая. К тому же оба эти решения выдают не оптимизированные изображения. Большинство других решений, например на Node предлагают использовать Sharp для ресайза, MozJPEG для оптимизации JPEG изображений и pngquant для оптимизации PNG.

    Мы и сами достаточно долгое время пользовались самописной связкой из Nod’ы, Libvips и MozJPEG c pngquant, но в один из дней задались вопросом— «А можно ли сделать ресайз быстрее и менее требовательным к ресурсам?».

    Спойлер: можно. ;)

    Теперь хорошо бы выяснить, как можно ускорить наше приложение. Изучив код приложения мы выяснили, что imagemin, который использовался для оптимизации, а в частности его плагины MozJPEG и pngquant при работе дергают одноименные утилиты через os.Exec. Будем это дело однозначно выпиливать и использовать только биндинги к Cи'шным либам. Для ресайза использовался модуль Sharp, который представляет собой биндинг к С библиотеке Libvips.

    Наша реализация


    Гуглеж показал, что Libvips по прежнему лидер по скорости и конкурировать с ним может только OpenCV. Значит будем использовать Libvips и в нашей реализации, это уже проверенное решение и он имеет готовый биндинг для Go. Пора попробовать написать прототип и посмотреть что из этого выйдет.

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

    Быстро написали прототип, протестировали и поняли, что несмотря на большее, чем в Sharp, количество внутренних крутилок, Libvips по-прежнему выдает на выход не оптимизированные изображения. С этим надо что-то делать. Опять обращаемся ко всемогущему гуглу и узнаем, что лучший вариант это по-прежнему MozJPEG. Тут начинают закрадываться сомнения, что мы сейчас напишем то же самое, что было на Node, только на Go. Но внимательно почитав описание MoZJPEG узнаем, что она является форком libjpeg-turbo и совместима с ней.

    Выглядит очень многообещающе. Дело за малым — собрать свою версию Libvips, в которой jpeg-turbo заменен на версию от Mozila. Для сборки мы выбрали Alpine Linux, т.к. приложение все равно планировалось публиковать с помощью Докера и Alpine имеет очень приятный формат конфига пакета, очень похожий на используемый в Arch Linux.
    Оптимизация картинки уменьшила ее размер в 4 раза без видимой потери качества.
    Оригинальный JPEG
    351x527
    79 Кб
    Оптимизированный
    351x527
    17 Кб
    greece_origin greece_optimized

    Собрали, протестировали. Теперь Libvips сразу при ресайзе выдает оптимизированную версию. То есть в Node версии версии мы сначала делали ресайз, а потом еще раз пропускали картинку через decoder-encoder. Теперь мы только делаем ресайз.

    С JPEG разобрались, а что делать с png. Для решения этой задачи была найдена библиотека libpngquant. Она не очень популярная, несмотря на то, что консольная утилита pngquant, которая базируется на ней, используется во многих решениях. Так же к ней был найден биндинг на Go, немного заброшенный и с утечкой памяти, пришлось его форкнуть починить, дополнить документацией и всем остальным, что подобает приличному проекту. Libpngquant мы тоже собрали в виде Alpine пакета для простой установки.

    Благодаря тому, что теперь изображение не требуется сохранять в файл для обработки c помощью pngquant мы можем немного оптимизировать процесс. Например не сжимать картинку при ресайзе в Libvips, а только после обработки в pngquant. Это позволит сохранить немного драгоценного процессорного времени. Надо ли говорить, что мы так же очень экономим благодаря тому, что вызов C библиотеки гораздо быстрее запуска консольной утилиты.

    Разница в размере в 3 раза, но возможно появление артефактов (зависит от картинки).
    Оригинальный PNG
    450x300
    200 Кб
    Оптимизированный
    450x300
    61 Кб
    bird_origin bird_optimized

    Пример не очень удачной картинки, на которой появляются артефакты при сжатии.
    Оригинальный PNG
    351x527
    270 Кб
    Оптимизированный
    351x527
    40 Кб
    greece_origin greece_optimized

    После того, как прототип был написан, протестирован на моем пк и выдавал приличные 25 RPS на мобильном двух ядерном проце, сжирая весь CPU, захотелось увидеть сколько можно выжать из него на нормальном железе. Запускаем код на шести ядерной машине, натравливаем Jmeter и WTF??? Получаем 30 RPS. Пробуем разобраться что за фигня.

    Libvips сам реализует многопоточность, то есть нам нужно только инициализировать библиотеку и в дальнейшем мы можем безопасно обращаться к ней из любого потока. Но у нас почему-то Libvips работает в 1 поток, что ограничивает нас одним ядром. Еще 1 ядро занимает pngquant. Итого получается, что наша супер быстрая ресайзилка отлично работает только на ноутбуке разработчика, а на остальных машинах не может утилизировать все ресурсы. ;)

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

    Все более или менее современные ядра Linux (3.9+ и 2.6.32-417+ в CentOS 6) поддерживают опцию SO_REUSE, которая позволяет использовать один порт нескольким экземплярам приложения. Данный подход удобнее, чем балансировка средствами стороннего ПО, такого как HAProxy, т.к. не требует конфигурации и позволяет быстро добавлять и убирать инстансы.
    Поэтому мы использовали SO_REUSE и опцию "--scale" в Docker compose, которая позволяет указать количество запускаемых экземпляров.

    Время мерить


    Пришло время оценить результат наших трудов.

    Конфигурация:

    • CPU: Intel Xeon E5-1650 v3 @ 3.50GHz 6 cores (12 vCPU)
    • RAM: 64 Gb (используется около 1-2 Gb)
    • Кол-во воркеров: 12

    Результаты:
    FIle Выходное разрешение Node RPS Go RPS
    bird_1920x1279.jpg 800x533 34 73
    clock_1280x853.jpg 400x267 69 206
    clock_6000x4000.jpg 4000x2667 1.9 5.6
    fireworks_640x426.jpg 100x67 114 532
    cc_705x453.png 405x260 21 33
    penguin_380x793.png 280x584 28 69
    wine_800x800.png 600x600 27 49
    wine_800x800.png 200x200 55 114

    Больше бенчмарков (правда без сравнения с Node версией) на wiki странице.
    Как видно переделывали ресайз мы не напрасно, увеличение скорости составило от 30 до 400% (в некоторых случаях). Если требуется ресайзить еще быстрее, то можно покрутить ручки «speed» и «quality» в libimagequant. Они позволят дополнительно сократить размер или увеличить скорость кодирования ценой потери качества изображения.

    Код проекта на GitHub.
    Биндинг Go к libimagequant так же на GitHub.
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 42
    • +2
      Почему приписка к картинке с JPEG «без видимой потери качества.» если по контуру здания на второй фотке появились артефакты сжатия?
      • –2
        Во первых да, примеры для статьи я пережимал с качеством 80, т.к. оно дает хорошее соотношение качество/объем. Естественно можно использовать более высокие настройки качества.
        Во вторых артефакты, о которых вы говорите, я заметил только при 500% увеличении картинки в браузере, но ее увеличение не предполагается.
        • +5
          Все зависит от глаз, монитора, освещения и других факторов… Мне видны артефакты без увеличения картинки.
          PS: Однажды долго доказывал автору скина аудиоплеера что на его черном скине имеются две слегка белые полосы. Он никак не мог понять где это, пока я не выложил картинку с задранным контрастом и яркостью…
          • 0
            Я вот не вижу никаких «артефактов по контуру здания», зато очень хорошо вижу, что небо превратилось в «мозаику» из таких слоистых «кусков». Довольно-таки неприятный эффект.
      • +3
        Только не надо путать ресайз с жатием изображения. Ресайз подразумевает изменение размера изображения — изменение высоты и ширины. И у ресайза есть свои проблемы и их решения.
        • 0
          Так я как раз и говорю о том, что для использования в вебе желательно не только ресайзить изображения, но и пережимать, потому что это может существенно сократить размер картинки. В своем решении мы совмещаем ресайз и пережатие, потому что только ресайз выдает достаточно большие по объему изображения.
          • +2
            в примерах картинки только с жатием, размер оставлен без изменения. И о проблемах при ресайзе ни слова — а это отдельная тема для большого разговора. Не надо путать людей заголовками.
            • 0
              Да, тоже зашел читать про ресайз — заголовок сбивает с толку. Статья при этом интересная, спасибо!
          • +2
            С png разница может быть еще заметнее. Разница в размере почти в 7 раз.

            Качество — ужас.
            • +1
              Ага. Не «возможно появление артефактов», а есть жуткие артефакты, видимые (на небе) невооруженным глазом.
              • 0
                Спасибо, действительно не самый удачный пример и на некоторых картинках пережатие работает не очень хорошо. Добавил пример хорошей картинки и немного поменял описание.
            • +1
              Пробывали модуль для nginx — google pagespeed?
              В нем есть отличный оптимизатор картинок вплоть до конвертации в WebP для современных браузеров.
              Мы используем ngx_http_image_filter_module для ресайза и pagespeed для оптимизации. Нагрузка минимальна. Полученные картинки конечно кешируются в pagespeed
              • 0
                А у вас он проксирует весь трафик или отдельный инстанс, который занимается только ресайзом? Какую примерно нагрузку он держит с кэшированием и без?
                • +1
                  У нас в принципе картинки ходят через отдельный инстанс. В настройках включена только оптимизация изображений. Без кеширования его включить нельзя. У него асинхронная модель оптимизации. На первый просмотр он всегда отдает оригинал.
                  Удобно тем что разным клиентам отдает разные оптимизации. Кому-то webP, а кому-то оптимизированный jpeg. Ну и конечно все рекомендованные оптимизации от гугла из коробки.

                  Сейчас пробывал очистить кеш pagespeed. На секунд 30 nginx скушал 4 ядра процессора и создал 1 гигабайт кеша за это время. После этого нагрузка сразу упала до привычных 20-30% У нас примерно по 200rps показы картинок. Но это веб сайт — кеширование результатов ресайза очень эффективно(больше 99% из кеша берется).
                  • 0
                    Мы естественно тоже кешируем результаты nginx'ом, но бывают например, что приходит какой то трафик на старые страницы, для которых кэша нет или сбросился кэш. Отсюда интерес к производительности без кеша.
                    Чем нам не понравилось решение на nginx- накрутить нашу логику в конфиге можно, но выглядеть это будет очень уж страшно. Новых проектов это конечно не касается, там больше свободы и можно и на nginx сделать красиво.
                    • +1
                      >> Чем нам не понравилось решение на nginx- накрутить нашу логику в конфиге можно, но выглядеть это будет очень уж страшно.
                      Ну я не знаю что страшней. 20 строк в конфиг nginx (локейшен для ресайза и парсинг аргументов — размеров + настройки кеша и pagespeed) или отдельный демон для ресайза.
                      Я конечно уверен что демон гибче и потому производительней. Но его ж нужно самим поддерживать, развивать и править баги.
              • 0
                В примере с jpeg разница между картинками действительно несущественна, хотя и можно заметить артефакты по границе башни, если присматриваться. А вот с png кошмар, картинка просто убита в хлам. И такая ситуация не «возможна», она стопроцентно будет, если ставить себе целью 7-кратный выигрыш веса.

                Если вам критичен вес картинок на сайте, внедряйте webp. Насколько мне известно, дает выигрыш порядка 20-30% при сжатии с потерями относительно jpg и 10-15% без потерь относительно png. Это уже при условии что jpg/png хорошо оптимизированы.
                • 0
                  Libvips дает возможность конвертировать картинки между разными форматами, в том числе в webp, но мы пока ей не пользуемся. За наводку спасибо, возможно стоит конвертировать в webp.
                • 0
                  А если делать по-серьёзному — то перед сохранением картинок на сервере, рекомендую сделать страницу, где юзер может произвести кадрирование изображения, поворот, ресайз, и сделать «полноразмерную» картинку для просмотра, и превьюшку.
                  • 0
                    Немного не наш кейс, у нас это работает например для таких случаев: аватарка пользователя хранится и показывается в профиле в 200х200, но на страницах форума показывается в 150х150, а например на главной в 75х75. И все эти уменьшенные версии надо генерить автоматом. Кроп, кадрирование и прочие фичи там не нужны.
                    • 0
                      фишка в том что при простом уменьшении размера с 200*200 до 75*75 произойдёт ухудшение качества изображения.
                      • 0
                        Не очень понял, что вы имеете в виду под простым случаем. Часть информации мы при ресайзе естественно теряем, но это не должно ухудшать качество картинки «на глаз»(естественно если потом получившуюся картинку не растягивать до размера оригинала).
                        Если же это проявляется в заметных артефакт, то есть смысл смотреть на алгоритм ресайза и его настройки.
                        На всякий случай уточню предыдущий коммент: мы храним только оригиналы в максимальном используемом разрешении (200х200 в примере). Менее качественные версии генерируются динамически и только кешируются на некоторое время.
                        Второй момент: например про главную. 75х75 это размер, определяемый версткой страницы, то есть в верстке есть контейнер 75х75 пикселей и мы вставляем туда версию нужного размера. Мы естественно не вставляем 75 версию в 200 контейнер.
                        • –1
                          «Простой ресайз» это изменение за один раз с 200*200 до 75*75. для исключения потери качества при ресайзе необходимо производить уменьшение в несколько шагов. Поэтому лучше хранить превью и полный размер. Полный размер просматривается на всегда, и его можно загружать по требованию. на это много времени не потребуется. и кэш для превью занимает меньше места.
                          • 0
                            Естественно, мы так и делаем. В моем коде это работает так:
                            Вместо "/test.png" в том месте страницы, где нужна маленькая версия мы пишем "/test.png@75". Все запросы с @ в конце отправляются nginx'ом на ресайз бэкенд. Он в свою очередь пасит url, скачивает оригинал ("/test.png"), ресайзит его до нужного разрешения и отдает nginx'у. Nginx кэширует результат и отдает его клиенту.
                            • 0
                              Ещё раз ресайзить за один раз — не правильно. происходит потеря качества.
                              не только потеря информации, но и потеря качества. А у Вас нет ни слова о том как происходит ресайз, хотя в заголовке написано про ресайз. Может в конкретном случае при таком достаточно не большом изменении размера это не заметно, но это так. Мне пришлось этим заниматься когда автоматизировал загрузку фото товаров на сайт. Было необходимо иметь просмотровую картинку 600*700 и превью 60*70. Алгоритм был отработан. А увидев заголовок про ресайз, решил что могу подчерпнуть что-то новенькое… к сожалению не удалось.
                              и из Вашего описания не получается что маленькая картинка берётся из кэша.
                              Все запросы с @ в конце отправляются nginx'ом на ресайз бэкенд.
                              получается, что все превью всегда ресайзятся
                              • 0
                                Сожалею, что не рассказал Вам ничего нового.)
                                Все запросы с @ в конце отправляются nginx'ом на ресайз бэкенд.

                                Nginx кэширует ответы бэкенда, и при следующем запросе она отдается уже из кэша, а не с бэкенда. В readme в репе есть пример конфига Nginx, который реализует эту логику.
                                По поводу ресайза в несколько проходов. Не могли бы Вы рассказать как вы это делали и какой библиотекой? На первый взгляд похоже на проблему с алгоритмом ресайза, из-за которой он при большом изменении размера портил качество. Теоретически вариант, когда мы ресайзим большую в маленькую должен быть лучше, чем вариант, когда мы делаем «большая->средняя->маленькая», т.к. не происходит потери на шаге «большая->средняя».
                                • 0
                                  Не могли бы Вы рассказать как вы это делали и какой библиотекой?
                                  библиотеки нет, есть простой алгоритм вычисления шагов, который определяет сколько требуется шагов для уменьшения в 2 раза и один шаг (1,x раз )для уменьшения до размера, чтоб следующие шаги уменьшали равно в 2 раза. далее уменьшение в 1, х раз и далее n шагов уменьшения в 2 раза. Это проверено при уменьшении картинки полученной с камеры (примерно 2к*2к) до превьюшки 60*70. библиотеки я не нашёл, поэтому пришлось делать самому. Весь процесс такой: есть рамка просмотрового размера 600*700, в неё загружается фото товара с помощью перемещения, поворота, изменения размера в эту рамку вписывается та часть фото, которая наиболее информативна. происходит «обрезка» — получается «просмотровая» картинка, дальше устанавливаются границы для превьюшки и так же кадрируется, «обрезка» и в результате 2 картинки. В процессе «обрезки» и происходит вся магия преобразования
                                • +1
                                  Это фигня из серии теплого лампового звука. Где-то когда-то что-то было, а теперь просто миф для «ценителей»
                                  • 0
                                    это к чему? к пошаговому ресайзу?
                                    • 0
                                      Да
                                      • 0
                                        спорить не стану, потому как прошёл это сам — если не веришь (в отличии от лампового звука, где важен слух) можешь сделать «ресайз», но только именно ресайз разовым уменьшением в N раз.
                                        • +2
                                          Я к тому, что разница может быть, но будет она только для некоторых старых алгоритмов, например, с тупейшим nearest neighbour внутри (потому что умножение в те времена стоило дорого).
                                          Современные алгоритмы, которые под векторные инструкции (SSE) спроектированы и написаны, в большинстве своём таких проблем не имеют. Можете взять какую-нибудь картинку в png (да хоть скриншот хабра) и сравнить побайтово результат уменьшения, скажем, в 10 или 20 раз одним махом или в 2-3 стадии.
                                          • 0
                                            тут дело не в векторных инструкциях. я ресайзил с помощью canvas, там алгоритмов как таковых нет. поэтому самый простой оказался с пошаговым уменьшением. и сравнивал с изменением в xnview с разными алгоритмами — если задавать разово большой коэффициент изменения — тоже качество страдает.
                                            интересно, что если в браузере уменьшать визуально(просто меняя количество точек для отображения) — качество отличное
                                  • 0
                                    Ещё раз ресайзить за один раз — не правильно. происходит потеря качества.
                                    Это очень сильно зависит от алгоритма масштабирования. Их много разных, от быстрых и грубых до медленных и качественных.
                                    В разном ПО и библиотеках могут быть реализованы очень разные алгоритмы, нужно выяснять, где и какой именно.
                                    Как примеры крайностей: в canvas ресайз очень сильно оптимизирован в сторону скорости, а качество полный шлак (там шаги насущная необходимость), в ФШ качество очень высокое (шаги — блажь и плацебо).
                                    • 0
                                      Не могу не поделиться. Недавно требовалось получить качественный пакетный downsample с больших (20-50-80 МП) оригиналов на 1080p и 2160p.
                                      Взяв ФШ в качестве эталона, попробовал opencv lanczos — на удивление не айс. Перепробовал разные варианты на opencv, перешел на imagemagic, следуя ряду рекомендаций, например, www.bvdwolf.nl/foto/resample/down_sample.html, удалось улучшить результат. Лучший вариант получился с RGB компенсацией и sinc фильтром.
                                      Но — ФШ все равно лучше! А пошаговое уменьшение в ФШ — еще лучше. Так и пришлось написать automation script пошагового уменьшения для ФШ. Хорошо, что сейчас это можно делать на питоне.
                                      Если кому-то известно, как ФШ этого добивается, или как сделать не хуже — будет крайне интересно. Если интересно — могу поделиться подробностями и картинками. В целом, надеялся, что в 2018 году все уже решено — ан нет, похоже, что тема для хорошей магистерской.
                                      • 0
                                        Лично у меня наоборот, удивление вызывает широко распространенное мнение о крутизне Ланцоша. Почему все решили, что он должен быть качественнее? Потому что там функция мудреная и график красивый? По мне так понятно, что он будет чудить своими осцилляциями. Возможно, за исключением каких-то специфических ситуаций.

                                        Ссылка любопытная, хотя тестовое изображение и сам тест кажутся слишком надуманными и искусственными.
                      • 0
                        странно. требуется производительность, многопоточность — и не используете GPU.
                        • –1
                          Кстати, да! Такая задача идеально ляжет на железо GPU.
                        • 0
                          а как вы используете SO_REUSE (PORT?) и --scale? scale же создает отдельные контейнеры с отдельными адресами

                          — А все увидел: вы запускаете контейнеры в хостовом netns
                          • 0
                            imlib2 не пробовали? Помнится у неё был оочень быстрый ресайз.
                            • 0
                              Судя по страничке Speed-and-memory-use libvips быстрее, но должен заметитить, что результаты imlib2 у них конечно немного странные и возможно они что то не докрутили. Более существенный, по моему мнению, недостаток imlib2- странная лицензия и достаточно маленькое комьюнити. Хотя погонять бенчмарк возможно все таки стоит.)
                              • 0
                                Судя по страничке Speed-and-memory-use

                                Скажите, зачем вы смотрите результаты, цитирую «load a TIFF image, crop 100 pixels off every edge, shrink by 10% with bilinear interpolation, sharpen with a 3x3 convolution and save again», если у вас задача качественно (то есть точно не bilinear) ресайзить в несколько раз (а не на 10%) JPEG (а не TIFF) и у вас нет шарпа, который занимает в данном тесте от 50 до 90% времени для разных библиотек?

                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                            Самое читаемое