Привет! Недавно мы провели небольшой митап для фронтендеров, куда пригласили троих интересных спикеров. Наш коллега Игорь Борзунов рассказал о том, как решать проблемы с плохим качеством изображений. Даниил Водолазкин из X5Tech поведал о неочевидных сложностях работы с GraphQL. И завершил программу Георгий Конюшков из «Леруа Мерлен» с темой «Time to market: микрофронтенды». В этом посте мы свели основные тезисы всех трех докладов.

Для начала — пара слов о том, что это у нас в банке за гильдии. Гильдии — это сообщества по профессиональным интересам, которые обмениваются знаниями, делают опенсорс-проекты в рамках контура банка, организуют менторинг и другие профессиональные активности. В том числе и митапы. А теперь слово спикерам.

Шакализация 21 века: решаем вечные проблемы изображений

На httparchive.org можно посмотреть статистику того, как менялся размер изображений за 11 лет развития интернета. В среднем он вырос с 225 до 979 Кб — и по моим наблюдениям это еще скромная оценка:

Теперь посмотрим на конкретные примеры разных лет. Вот картинка с сайта CNN тех лет — достаточно четкая для своих размеров, все отлично видно:

А вот пример современной иллюстрации с новостного портала «Афиша Daily». Размер несколько больше и объем достигает уже 2,2 Мб:

Почему размер картинок так быстро растет? Есть несколько причин. Появляются новые кодеки, которые пытаются сделать изображения качественней, в том числе за счет занимаемой памяти.

Вторая причина в том, что с каждым новым сохранением одной и той же картинки увеличивается ее объем (и за счет появления новых артефактов снижается качество). Фронтендеры традиционно с этим не парятся — в СНГ мы привыкли к скоростному интернету. Ну подождут люди с 3G чуть дольше, ничего критичного.

С этим связан случай из жизни. Я работаю в команде депозитов нашего интернет-банка для юридических лиц. В разговоре с одним клиентом я узнал, что у него уходит 3–4 минуты, чтобы разместить депозит в нашем приложении. Это показалось мне странным, я привык, что эта операция занимает несколько секунд. Я начал расспрашивать подробности: оказалось, что скорость соединения клиента составляет в лучшем случае 2–3 Мбит/с. Я ограничил в devtools скорость своего интернета и решил пройти этот путь глазами пользователя.

Вот отчет по скорости загрузки ассетов. JPEG в 48 Кб грузится секунд шесть, 332 Кб — уже секунд 15. И это только экран авторизации. Дальше свои картинки есть у главной страницы, у разделов… так и набегает несколько минут. Со скоростным же интернетом все гораздо оптимистичней:

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

Что с этим делать? Первое, что приходит в голову — рефакторинг или исправление багов. Долго, дорого и не факт, что чего-то добьемся. Можно свалить вину на бэкенд :) Или… оптимизировать приложение. Давайте остановимся на последнем варианте и продумаем, что можно сделать в рамках этой оптимизации:

  • изменить подход к форматированию изображений, проанализировать, какие вообще есть изображения;

  • пережать все еще раз;

  • лучше встроить графику в экосистему проекта.

Остановимся пока на сжатии изображений. Будем говорить, в основном, о сжатии с потерей качества, хотя есть методы и без потери. Начнем, конечно, с самого популярного формата — JPEG, в котором закодировано 90% всей интернет-графики. Сжатие в JPEG происходит в несколько этапов:

  • замена цветового формата,

  • сабсемплинг,

  • блочное деление,

  • преобразование блоков,

  • квантизация.

Есть и другие, более новые форматы, которые также становятся популярными — WebP и Avif. Здесь встает вопрос поддержки форматов различными браузерами:

WebP поддерживается всеми браузерами кроме IE и некоего KaiOS. В целом, приемлемо. Чем отличается формат WebP сам по себе? Он работает примерно так же, как JPEG, но с некоторыми техническими фишками типа предикшенов и зигзаг-сравнений.

Теперь Avif — чуть более современный и менее популярный:

Поддержка браузерами здесь примерно в два раза хуже, чем у WebP, и это уже становится проблемой. Safari, Opera, Edge — за бортом. Теперь о том, как работает Avif:

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

Если сопоставить три формата по размеру, то WebP в среднем получается на 30% компактнее JPEG. Но Avif еще меньше JPEG — в среднем почти в два раза! Avif здесь предпочтительней, посмотрим теперь на характеристики сжатия:

У JPEG нет никакого предсказания в алгоритме, все довольно топорно, а также имеется только одна произвольная матрица, без зигзаг-сравнений, которые я упоминал выше. Везде используется дискретно-косинусное преобразование, но у WebP и AviF есть также другие возможности.

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

const ImageminWebpWebpackPlugin= require("imagemin-webp-webpack-plugin");

module.exports = {

    plugins: [new ImageminWebpWebpackPlugin()]

};

Посмотрим, сколько плагин сэкономил места. Локально экономится чуть меньше, в продакшене чуть больше. У нас в проекте получилось 44 Мб, и это уже неплохо.

 Для сравнения я протестировал и компрессию в Avif:

Стоит оговориться, что даже сами разработчики Avif не рекомендуют использовать его в продакшен-средах из-за текущей сырости и нестабильности. Вроде бы очевиден выбор в пользу WebP, но есть проблема — отсутствие поддержки Internet Explorer. А это все-таки второй по популярности браузер у наших клиентов.

Проблему можно решить через middleware, а точнее, с помощью nginx. Берем nginx-конфиг и через map указываем, что у нас есть WebP, а потом на уровне сервера через location и try_files подставляем нужные суффиксы и получаем Web

http {
  map 
default        "";
    "~image/webp"  ".webp";
  }
}
server {
  # […]
  location ~ ^/images/.*\.(png|jpe?g)" class="formula inline"> {
    add_header Vary Accept;
    try_files webp_suffix webp_suffix $uri =404;
  }
}

В результате вместо JPEG в content type у нас будет WebP:

Теперь для всех современных браузеров с поддержкой WebP мы отдаем этот формат, а остальные получают тот же JPEG.

Вот эта картинка на главной интернет-банка весит в JPEG 48 Кб, а в WebP — 30 Кб. Разница очевидна, и это только одна картинка из сотен.
Вот эта картинка на главной интернет-банка весит в JPEG 48 Кб, а в WebP — 30 Кб. Разница очевидна, и это только одна картинка из сотен.

Какие выводы можно сделать?

  1. Сжатие изображений полезно для всех. Даже в строгом интернет-банке польза вполне ощутима, не говоря уже о более художественных сайтах с гифками-анимациями-видео.

  2. Новые форматы все еще слабовато поддерживаются.

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

А теперь — секция вопросов и ответов:

  • Разве не лучше использовать для сжатия SVG? — Не все можно легко преобразовать в векторную графику, к тому же на больших картинках экономии почти не будет.

  • Нормально ли сжимать картинки на уровне клиентского приложения после того, как их выбрали из файловой системы и отправили на бэкенд? — Нормально :)

  • Были ли ситуации, что переформатированные в WebP картинки весят больше, чем PNG? — Не было.

  • А как насчет Progressive JPEG? — У разработчиков формата много новых наработок, но здесь также возникает проблема в поддержке браузерами. Яркий пример — JPEG XL. К тому же многие пишут про сырость этого алгоритма. Так же как и с Avif, о внедрении на продакшен говорить рановато, но поиграться можно.

  • Если пользователь захочет скачать с вашего сайта картинке в формате WebP, нужно ли будет что-то дополнительно? — Нет, браузер сделает всё сам без проблем.

  • Насколько в итоге уменьшилось время загрузки интернет-банка? — В среднем на одной странице время загрузки уменьшилось на 10–30%, если судить по синтетическим тестам dev-tools.

Неочевидные сложности в работе с GraphQL

В этом выступлении я поделюсь опытом работы X5Tech с GraphQL на примере системы планирования ассортимента торговой сети «Пятерочка», самыми классными моментами и также проблемами, которые возникли в неожиданных местах.

Торговая сеть насчитывает 17600 магазинов в 7 федеральных округах России с ассортиментом от 3000 до 7000 наименований в каждом. Как следствие, мы получаем очень много данных. У нас есть bigdata-команда, которая собирает нужные показатели, аналитику, прогнозы и т.д. Мы на фронтенде работаем с такими сущностями как, например, товары. У каждого может быть сотня показателей, которые нужно отображать, модифицировать. Кроме того, с рядом сущностей приходится работать в реальном времени, то есть иметь всегда свежие данные.

Почему мы выбрали GraphQL? Когда-то мы начинали с обычным REST API, но постепенно пришли к тому, что в разных местах нам нужно по-разному преобразовывать похожие данные. В результате API стал обрастать большим количеством ручек и мы встали перед выбором:

  1. Делаем ручки под каждую потребность, что приводит в долгой доработке бэкенда под каждую фичу.

  2. Объединяем данные из атомарных запросов, что сильно нагрузит клиент для обработки данных.

Оба пути нас не устроили, поэтому было решено отказаться от REST. В этой ситуации переход на GraphQL дал сразу несколько важных преимуществ:

  • Уменьшились затраты на разработку бэкенда для каждой отдельной фичи. 

  • Исчезли проблемы с over-fetching и under-fetching. У нас много данных, есть таблицы в десятки тысяч строк — в таких условиях приходится учитывать любые лишние данные в запросах.

  • Мы получили готовую инфраструктуру — Apollo-сервер, Apollo-клиент, кэш, подписки, обработка ошибок, сетевые статусы и т.д.

Как основу для демонстрационного приложения я использую The Rick and Morty API. Он умеет и в REST, и в GraphQL, предлагает удобную площадку для экспериментов, всем советую. На площадке вы можете запрашивать данные по известному мультсериалу. Вот как примерно выглядит подробно описанный запрос:

query {
  characters {
    results {
      id
      name
      status
      species
      type
      image
      episode {
        id
          name
          air_date
          episode
          created
        }
      created
    }
  }
}

И вот такой такой ответ можно получить:

{
    "data": {
        "characters": {
            "results": [
                {
                    "id": "1",
                    "name": "Rick Sanchez",
                    "status": "Alive",
                    "species": "Human",
                    "type": "",
                    "gender": "Male",
                    "image": "https://rickandmortyapi.com/api/character/avatar/1.jpeg",
                    "episode": [
                        {
                            "id": "1",
                            "name": "Pilot",
                            "air_date": "December 2, 2013",
                            "episode": "S01E01",
                            "created": "2017-11-10T12:56:33.798Z"
                        }
                    ]
                }
            ]
        }
    }
}

Одна из самый классных фичей GraphQL — это типизированный API, который позволяет точно понять, что вы получите на выходе. Сообщество GraphQL советует всегда как можно точнее указывать типы. Но попробуем вот что. В приложении для query я могу выставлять, информацию по какому количеству персонажей мне нужно получить. Выставляем, например, 100 персонажей, и посмотрим, сколько займет ответ на подробно описанный запрос: 

На ответы ушло 160–260 мс. Теперь попробуем провести другой запрос. Он будет брать такой же объем данных в том же месте, но будет прописан просто как json, без всяких типов. Весь код запроса — на трех строчках: 

Время получения той же сотни строк уменьшилось в несколько раз, стоило нам обойтись без подробностей в запросах. Увеличив число строк до 1000, мы сохранили ту же кратную разницу в скорости ответа — в 4–5 раз. Возможно, на другом API мы получим чуть иное соотношение, но общий итог поменяется вряд ли. Есть подозрение, что увеличение времени связано с наличием валидации типов. То же самое было и у нас в проекте: согласно лучшим практикам, мы старались типизировать ответы, но в итоге частично их пришлось упростить. Тем не менее у типизации, конечно, есть преимущества. У Apollo есть свой code generator, который отлично все упрощает.

Теперь перейдем к фронтенду. У нас есть приложение, в котором мы получаем данные о персонажах — id, имена и ссылки на картинки:

Источник: rickandmortyapi.com
Источник: rickandmortyapi.com

При клике на персонажа получаем о нем более подробную информацию: показываем вид, статус, пол. Проходит новый запрос:

Источник: rickandmortyapi.com
Источник: rickandmortyapi.com

Переключатель над персонажами — это описание network policy для запросов: cache-first, cache-only и т.д. Мы получили ответы по методу cache-first: Apollo сходил в кэш, не нашел там данных и запросил их с сервера.

Если выставить cache-only, то по новым персонажам мы не получим ответа, так как в кэше их нет. Более интересен метод cache-and-network, когда Apollo сравнивает данные из бэкенда и кэша. В network-only обращения к кэшу нет, а при no-cache мы вообще ничего в кэш не кладем.

Apollo отслеживает параметры запросов, если вводные совпадают, данные перезаписываются. Каждый запрос с новыми параметрами — это новая запись в кэш. Это привело нас к проблеме на одной сводной таблице в 240 столбцов и несколько тысяч строк. Чтобы это все не показывать пользователю сразу, мы нарисовали фильтры, которые передаем на бэкенд через json. В ответ может прийти до 100–150 Мб — после нескольких перезагрузок этих фильтров браузер пользователя переставал отвечать. Решили вопрос, установив политику no-cache —  в той задаче кэширование было некритично.

Что в итоге можно посоветовать в работе с Apollo Client?

  • В третьей версии появился Garbage Collector, который подчищает кэш (в нашей истории он бы пригодился). Не забывайте о нем.

  • Следите за network policy.

  • Используйте простые параметры query и дробите их на бэкенде по необходимости.

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

Не могу не отметить, что есть практика использования Apollo Client как state manager. Посмотрите видео о нем, может, он вам подойдет.

В заключение расскажу о концептуальных сложностях, с которыми мы столкнулись при внедрении GraphQL.

  • В GraphQL отличается парадигма построения API, бывает непросто выстроить эту древовидную структуру.

  • Если вы используете для подготовки GraphQL BFF, бэкенд не всегда сможет поставлять вам данные удобным образом — с пагинациями и т.п.

  • Если вы решили внедрить GraphQL на Node.js, то поломка BFF обернется для фронтендера проблемами, с которыми он раньше не сталкивался. Но до этого времени все будет отлично.

Если вы хотите посмотреть все самостоятельно, вот демка, которую я использовал выше.

Time to market: микрофронтенды

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

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

Основным нашим инструментом был Adobe Experience Manager — очень дорогая монолитная CMS-ка на Java с малым количеством специалистов, которых даже приходилось искать в других странах СНГ. Продакт быстро закидывал разработчиков задачами, но их было мало и поэтому дедлайны соблюдались плоховато.

Это нас не устраивало, и мы начали осторожно присматриваться к микрофронтендам. Какие проблемы они решают?

  • Релизы без конфликтов. Микрофронтенды обособлены, каждая команда пилит свой.

  • Удобный выбор технологии. Каждая команда может выбрать удобные инструменты и использовать их.

  • Эффективное A/B-тестирование.

Вот иллюстрация того, как это может выглядеть в интернет-магазине:

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

Архитектура микрофронтендов нам понравилась, и мы обсудили ее с коллегами из бизнес-подразделений. Вот их требования:

  • сохранение SEO (оптимизации для поисковиков), мы же все-таки интернет-магазин;

  • сохранение SSR (server-side rendering);

  • использование React для UI/UX, потому что это легко и удобно;

  • возможность A/B-тестирования.

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

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

Выбрали имя для новой платформы: в итоге остановились на Okapi. Окапи — это лесной жираф, редкое животное, в котором смешались многие представители парнокопытных. Символично получилось :)

В базовой схеме приложения у нас есть клиент, который запрашивает приложение (App) и его ресурсы, а затем делает между ними связку (Service Discovery). Кроме того, мы сохранили AEM (Adobe Experience Manager) Dispatcher, так как он отлично подходит для создания контента и загрузки его на нужные страницы. Клиент заходит на наш сайт, AEM Dispatcher отправляет его в нужное место.

Нам, конечно, пригодится CDN, так как все наши файлы хранятся отдельно друг от друга в своих микрофронтендах под хешем. Дальше все это обрастает BFF-ами, которые фильтруют данные для каждого приложения из общего множества. У нас есть своя разработка, которая позволяет быстро создавать новые BFF чуть ли не простым драг-н-дропом. Также есть remote-компоненты, о которых мы поговорим позднее.

Рассмотрим более подробно сам Okapi. В него входит:

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

  • Server-side рендеринг.

  • Client-side рендеринг.

  • Remote-компоненты.

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

У нас есть страница App 1, на ней список товаров Related products. Его можно перезапросить и использовать на App 2 без повторного рендеринга, как обычный компонент React — с отрисовкой, рабочими SSR и клиентской частью. В виде таких компонентов можно использовать весьма крупные сущности, такие как, например, товарную выдачу. Как мы их реализовали?

У каждого такого компонента есть своя клиентская, серверная и бизнес-логика, где реализуются запросы данных к BFF, а также UI/UX. Как это использовать в другом приложении?

Здесь в игру вступает Remote Provider, который позволяет нам забрать данные у одного приложения и переслать другому. Вот так remote-компонент используется и может быть передан: 

<Remote
widget="microfront_name/widget_name" // Откуда брать компонент
data={{ ids: ['1', '2'] }} // Контракты
fallback={() => <div>Something went wrong</div>} // Что-то пошло не так
/>

Чем для нас оборачиваются микрофронтенды? Начнем с преимуществ:

  • Множество команд работаю автономно и быстро, сами формируют релизы, им не нужно договариваться, чтобы выкатить какой-нибудь A/B-тест.

  • Конфликт релизов отсутствует.

  • Быстрая доставка фичей в течение дня.

  • Просто искать разработчиков — под капотом React,  в довесок к нему нужно знать особенности SSR, которые можно быстро понять в процессе работы

  • Полная поддержка SSR.

  • Простое переиспользование компонентов — сегодня это наша киллер-фича.

Без недостатков тоже не обошлось:

  • Все-таки у нас не совсем микрофронтенды, мы дробим интернет-магазин достаточно крупно, на целые страницы

  • Нет полной свободы выбора технологий при создании приложений — сейчас работаем только с React.

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

И, пожалуй, главный минус: каждый запрос remote-компонента идет отдельно. Если у вас, например, товарная выдача составляет 30 компонентов, и вы делаете 30 запросов выдачи… это уже не очень хорошо. Мы пробуем бороться с этим, например, путем склеивания запросов. Если вас заинтересовала тема, вы можете узнать больше о реализации микрофронтендов в нашем блоге.

В одном из следующих постов мы расскажем о выступлениях на IT Architecture Meetup Росбанка. Не пропустите!

Комментарии (0)