Как мы решили оптимизировать картинки — а в процессе переделали сайт, админку и подход к интерфейсу

    Рассказывает технический директор «Медузы» Борис Горячев.

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



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

    • картинка должна быть загружена в CMS (мы ее называем «Монитором») максимально быстро
    • картинка должна оставаться красивой и хорошо выглядеть на всех платформах
    • читатель не должен ждать загрузки этой картинки


    Первый подход


    Когда мы запускались в 2014 году, процесс работы с загруженными в «Монитор» картинками выглядел так: файл загружался в Rails-приложение, с помощью Paperclip и Imagemagick он очищался от метаданных, сжимался с выбранным качеством (для JPEG это было в районе 75) и нарезался на три размера: маленький для телефонов, побольше для планшетов и телефонов с большими экранами, и совсем большой для компьютеров. Нарезанные файлы укладывались в облачное хранилище AWS вместе с оригиналом. Отдавались (и отдаются) они не напрямую с AWS, а через наш CDN, который кэширует его на своих edge-серверах.

    API, формирующее JSON для сайта и приложений, тогда кроме простых атрибутов, таких, как заголовки материалов, отдавало еще и огромный кусок HTML-кода, который вставлялся в «контентную» часть материала и обвешивался CSS-стилями. А само API тогда было единым для всех клиентов: сайта, приложений и поддерживающих сервисов, вроде RSS и поиска.



    Нам сразу было понятно, что такой подход не выдержит проверки временем, но нужно было запускаться быстро, проверять огромное количество гипотез, экспериментировать, выживать. Мы осознанно — по крайней мере, мы в это верим — выбирали «костыли», а не красоту кода и решений. Время шло, росла наша аудитория и наш продукт. А вместе с этим росли аппетиты редакции. Редакции хотелось все больше приемов и «фишек» в своих материалах. В то же время, конечно, менялся и дизайн.

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

    Вот одно из них


    Со временем Монитор все сильнее бесил всех, кто с ним сталкивался: от багов страдала редакция, из-за того, что починка этих багов занимала очень много времени, бесились разработчики, а ограничения придуманной архитектуры не устраивали начальство — мы не могли быстро развивать продукт.

    Мы начали переделку Монитора. Основную часть CMS, отвечающую за работу над материалами, мы переписывали примерно год, регулярно отвлекаясь на баги в старой версии. Тогда в «Медузе» появился внутренний мем: «Это будет в новом Мониторе». Так мы отвечали на большинство просьб редакции, хотя, конечно, какие-то вещи делали одновременно в старой и новой CMS: плохая версия на сейчас и хорошая на потом.

    Подробно про переделку «Монитора» я скоро напишу отдельный пост в нашем блоге.

    Перезапуск


    После перезапуска и пересборки всей «Медузы» API оставалось прежним по формату, но оно больше не формировалось самой CMS, а обрабатывалось отдельным сервисом. И мы решились наконец разделить API на несколько — под разные клиенты.

    Начать решили с мобильных приложений. К тому моменту к ним уже были вопросы по дизайну, UX и скорости работы, так что мы прибрались в приложениях и сделали API таким, каким его хотели наши iOS- и Android-разработчики.

    До разделения API мы показывали контентную часть материалов через WebView, поэтому мы не могли что-то показывать исключительно в приложении или исключительно на сайте — все отображалось везде. Теперь у нас появилась возможность более гибко управлять контентом: отдавать в мобильные приложения только то, что нужно, по-другому показывать тяжелые элементы (такие, как вставки роликов с ютьюба), и наконец сделать Lazy Load, позволяющий постепенно подгружать в материал тяжелые элементы — картинки и эмбеды.

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

    Так появился UI-kit Медузы


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

    Возьмем, к примеру, игры «Медузы». Они позволяют рассказывать огромное количество историй в игровой форме. Они могут быть сделаны специально под повестку или под запрос рекламодателя. Или игра может быть сделана на основе так называемых механик — форматов, которые используются многократно (например, это тесты).

    Игры на «Медузе»: как мы их придумываем, делаем и переиспользуем
    Код этих игр не лежит в коде сайта. Каждая из них создается как отдельный микросервис, встраиваемый на сайт через iframe и общающийся с самим сайтом через postMessage. И сайту вообще все равно, что показать в том месте, где будет игра. При этом сама игра должна быть визуально неотделимой от сайта: типографика и элементы интерфейса должны быть одинаковыми.



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

    Команда решила, что это надо поменять. Мы поставили на паузу переписку сайта и начали делать свой UI-kit — библиотеку, в которую вошли все повторяющиеся элементы и стили. Мы старались не упустить длинную перспективу: и сайт, и игры, и механики — все должно было начать использовать одну библиотеку.

    Все проекты «Медузы», работающие в вебе, написаны на React, и UI-kit — это npm-модуль, который сейчас подключается почти ко всему, что мы разрабатываем. И разработчик, когда ему надо отрендерить что-то, пишет примерно так: render blocks.



    Что это дает?


    1. Фронтэнд-разработчик не думает, как именно что-то отобразить.
    2. Все уже отсмотрено дизайн-отделом.
    3. Редизайн проходит максимально безболезненно: сначала он происходит в UI-kit, его отсматривают дизайнеры, тестируются крайние значения, а потом проекты поочередно обновляются на нужную версию. В итоге все компоненты, из которых состоит «Медуза», выглядят и работают единообразно.
    4. Дисциплинирует дизайнеров и разработчиков — появление чего-то необычного вне UI-kit (и тем более добавление элемента в кит) должно быть аргументировано.

    Устройство UI-kit


    Есть две группы компонентов: контентные и интерфейсные. Вторые — это очень простые React-компоненты, такие, как кнопки и иконки.





    Контентные компоненты — чуть сложнее, но при этом все равно очень простые React-компоненты со стилями, которые представляют собой одну единицу контента. Например, вот так выглядит компонент параграфа:



    Компонент, который «ловит» простые блоки



    А так — компонент, который рендерит картинку:



    API, из которого сайт берет данные, внутри каждого материала содержит массив компонентов, которые в итоге «рендерятся» через UI-kit. С играми все работает так же, просто они ходят за своими данными в свои версии API.

    Ура, мы перезапустились!


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



    На графике можно видеть, как менялась скорость сайта за все время существования «Медузы». Когда мы только запустились с очень простым сайтом в 2014 году, он работал очень быстро. Но когда мы начали добавлять новые функции, скорость загрузки упала.

    А вот такой же график, но за последние два года. На нем видно, как после перезапуска сайта время загрузки страниц снизилось.



    Тут наконец настало время картинок.

    Картинки


    Схема работы с изображениями тогда была такой. Картинка приходила через API, где у нее был адрес вида /images/attachments/…/random.jpg. Сам файл отдавался из облачного хранилища AWS через наш CDN.



    Требования к новой системе мы сформулировали так:

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

    Схема, к которой мы стремились, получалась такой. Бэкенд формировал бы URL, который бы забирался клиентом — браузером или приложением. В URL содержалась бы информация о том, какая картинка нужна, в каком качестве и каких размеров она должна быть.

    Если картинка по такому URL уже есть на Edge-сервере, она бы сразу отдавалась клиенту. Если нет — Edge-сервер «стучался» бы в следующий сервер, который уже передавал бы запрос в сервис. Этот сервис, получив URL изображения, декодировал бы его и определял адрес оригинальной картинки и список операций над ней. После этого сервис отдавал бы трансформированную картинку, чтобы та сохранялась в CDN и отдавалась по запросу.



    В «Медузе» уже есть похожие решения. Например, мы похожим образом делаем картинки для сниппетов наших материалов и игр в соцсетях — в этом сервисе мы делаем скриншоты HTML-страниц через Headless Chrome.

    Новый сервис должен был уметь работать с изображениями, применять простые эффекты, быть быстрым и отказоустойчивым. Так как мы любим все писать сами, изначально планировалось написать такой сервис на языке Elixir. Но ни у кого в команде не было достаточно времени, и уж точно ни у кого не было желания погружаться в дивный мир jpg, png и gif.

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

    А еще нам нужна была поддержка формата картинок webp.

    Время шло, мы морально готовились к тому, чтобы погрузиться в этот проект. Но тут кто-то из наших программистов прочитал про библиотеку imgproxy, которую выложили в Open Source ребята из Evil Martians.

    По описанию это было идеальное попадание: Go, Libvps, готовый Docker-образ, конфигурирование через Env. В этот же день мы развернули библиотеку на своих ноутбуках и попросили нашего DevOps тоже с ней поиграть. Его задача была в том, чтобы поднять сервис и попытаться убить его — так мы бы поняли, какие нагрузки он выдержит на наших серверах. В это время бэкенд-команда продолжала баловаться с проектом на своих компьютерах: мы писали Ruby-скрипты и осваивали доступные функции.

    Когда DevOps вернулся с вердиктом, что библиотеку можно использовать в продакшене, мы собрали большое количество картинок, пропущенных через imgproxy — нас в первую очередь интересовал webp — и понесли их фотодирекции. Сотрудники техотдела не могут сами решить, подходит ли нам такое качество или нет. Фоторедакторы передали нам свои замечания, мы что-то подкрутили, посмотрели, чтобы изображения не весили много, и пошли писать бэкенд-код.

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





    Обновленное сразу поехало в продакшен, так как мы ничего не изменяли, а только добавляли — ребята на фронтэнде могли разрабатывать функции все в той же версии API. Они расширили компонент картинки по функциям, добавили туда наборы изображений под разные размеры и специальный «костыль» для Safari. Мы пару раз меняли таблицу размеров и сверяли результат глазами редакции — для этого давали ей на отсмотр текущую версию сайта и дублирующую с новыми изображениями.

    Когда замечания перестали поступать, мы финально задеплоились, сбросили кэш и выкатились в продакшен.

    Вывод


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

    Сейчас почти все картинки, которые вы видите на «Медузе» — результат работы imgproxy, и в каждом случае у них разные размеры и иногда разное качество. Это определяется контекстом — открыт ли материал в вебе, мобильном приложении или AMP — о котором знает API-сервис, формирующий ответы.
    Meduza: dev
    Мы разрабатываем «Медузу»
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 19

      0
      Если вы дочитали до конца и дошли до комментариев, то во-первых спасибо, а во-вторых — можете писать, какие темы и какая информация о работе большого медиа вас интересует. Это поможет нам лучше спланировать следующие публикации
        0
        Спасибо. По архитектуре бы почитать. Что было, что сейчас. Как переходили и почему?)
        В принципе по статьям примерно понятно, но все же…
          0
          Я планирую написать про архитектуру cms и api (но пока не буду давать обещаний)
        0

        Раньше хватало трёх размеров изображений (кстати, а сколько точно в пикселях?), а сейчас сколько в ходу? Те же три или стало больше?

          0
          у нас в до-ретино-мобильную эру был один размер, щас от 20 до 32, в зависимости от того, как должна вести себя картинка при разной ширине…
            +1
            Откуда могут взяться 32 размера??? Господи, шаг 40 пикселей до 1200px ширины? Куда так нарезать то? Звучит как — фоток, нарезанных с шагом по по 10px хватит каждому. Похоже какое-то легаси. Неужели нельзя отдавать до 10 штук и попадать в размеры уже ужиманием картинки версткой?

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

            Превьюшка с главной. Экран 1920px

            image

            И еще.
            jpg 144kb

            meduza.io/image/attachments/images/005/714/465/wh_615_410/RGElDSRNdR5rF8COafVHhQ.jpg

            webp 74kb
            meduza.io/impro/uCSx7EjLksLrJSIfXDg1u-9mDRMr_amVlO5-Qr5SVqw/fill/0/0/ce/1/aHR0cHM6Ly9tZWR1/emEuaW8vaW1hZ2Uv/YXR0YWNobWVudHMv/aW1hZ2VzLzAwNS83/MTQvNDY1L3doXzYx/NV80MTAvUkdFbERT/Uk5kUjVyRjhDT2Fm/VkhoUS5qcGc.webp

            Затащил ваш jpg в фотошоп. Пережал до 75kb — получил визуально идентичное фото. Рисунок вен на руке только у парня имеет разные артефакты, свойственные энкодерам. Понятно, что на одном файле не тестят. Но я уже баловался сравнялками с webp — был неприятно удивлен тем, что webp просто отвратительно работает с градиентами и его преимущества очень спорные. Думаю, не дожимаете вы jpg.
              0
              неудачно написал. Я не из медузы, про проект своей конторы говорил. Ну и нарезать 32 картинки конечно же не нужно, параметры ресайза достаточно задать в урле. Хоть тыщу разных
          0

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

            0
            Много факторов. Знание команды, популярность, стабильность, требования по нагрузкам.
            +5
            Дорогая редакция, подскажите, а код текстом нынче это немодно, нестильно, немолодёжно? Обязательно всем показать, что у вас Mac? Про кликабельность картинок я уже молчу…
            +3
            Сколько гигабайт ram нужно хрому для показа вашей страницы?
              +1
              Imagemagick, с которым у нас уже был опыт, был не самым быстрым решением

              Значит вы больше не генерируете картинки в разных размерах заранее, но вместо этого используете imgproxy, который делает это по запросу? А чтобы не генерировать картинку заново на каждый запрос, они кешируются на ваших cdn серверах?
              А какие решения существуют для трансформации изображений быстрее Imagemagick?
              +3
              Медуза это прекрасный образец оверинжниринга. Сайт как раньше еле шевелился на мобилках, так и сейчас…
                0

                Уж не собираетесь ли вы выложить вашу CMS в open source, или, быть может, продавать лицензии на неё?
                Я встречал некоторые СМИ, сделанные по вашему образу и подобию, и их сайты имели значительно больший объём главной и грузились медленнее, чем ваш.


                И разрешите придраться. В статье довольно много web-сленга, которой очень странно смотрелся на Медузе, пусть на Хабре это и выглядит нормально. Но отдельные фразы (я ниже выделил жирным), похоже, на каком-то другом языке, отличном от русского.


                где лучше учитывается контекст и проще реализуется имплементация и поддержка.

                На эту (имхо) ошибку я указывал средствами Медузы, но она не исправлена, и там нет обратной связи. Что вы хотели сказать этими словами?

                  0
                  Мы специально помечаем статьи про нас самих тегом медуза на сайте. увы, но по-другому объяснить было бы сложно.
                  0

                  Я в своем проекте, столкнувшись с такой же ситуацией, просто написал лямбду на ноде, которая с помощью imagemagick на лету отдавала из s3 Амазона картинки, в зависимости от переданных параметров.


                  Все это покрыто кешированием, включая кеширование отдельных параметров (width, height...)


                  Если я ничего не упускаю, то в итоге я могу сформировать схему в api подобную Вашей, имея всего одну-две картинки-оригинала в хранилище и все будет работать +- одинаково)


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


                  Кстати спасибо за наводку про imgproxy, сейчас сам ее тестировать буду)

                  Only users with full accounts can post comments. Log in, please.