Микрофронтенды: зачем дробить фронтенд и почему это может быть хорошо
Обзорную статью о том, как дробить фронтенд, и почему это хорошо, заказывали? Их есть у меня!
Привет, Хабр! Меня зовут Александр Гончаров, и я — Head of Frontend в ГК Юзтех. В коммерческом IT я c 2011 года, регулярно выступаю как спикер на конференциях и митапах, а также с 2014 года веду подкасты, в том числе «Суровый веб». Мы в Юзтехе занимаемся заказной и продуктовой разработкой с 2006 года, в моём отделе около 80 человек, а в команде текущего проекта — 50 разработчиков.
Ещё любопытные факты с цифрами: у меня три высших образования (также сейчас учусь в аспирантуре), четыре кошки, более 300 записанных выпусков подкастов и больше 200 проведенных стримов.
Эта статья — адаптация моего доклада, который я рассказал на TechleadConf 2024. В ней я не буду углубляться в технические детали реализации микрофронтендов и архитектурных паттернов, а вместо этого разберу, как они могут помочь командам работать быстрее и без боли.

Поуправляю вашими ожиданиями и заранее предупрежу, что будет в этой статье:
-
Какие проблемы есть в крупных проектах и как микрофронты их решают?
-
Немного базы: как устроены микрофронтенды с точки зрения архитектуры?
-
Почему внедрение микрофронтендового подхода помогает разделить ответственность и ускорить поставки?
Но в первую очередь определимся с понятием Микрофронтендов. Погнали!
Что такое микрофронтенды?
Микрофронтенды — это фронтенд-приложения внутри фронтенд-приложений. И да, мы буквально можем встроить фронтенды в ваши фронтенды, чтобы смотреть фронтенды.
Говоря проще, это архитектурный подход, при котором большое пользовательское приложение разбивается на независимые изолированные модули (микроприложения). Эти модули можно разрабатывать и деплоить отдельно, а затем магическим образом собирать в единое целое для пользователя.
Делить микрофронтенды можно горизонтально или вертикально, а затем интегрировать.

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

С понятиями разобрались, теперь поговорим о проблемах.
Проблемы больших команд (и небольших, но проблемных команд) подробно с примерами
Когда приходите в большую команду или проект, не надо сразу предлагать всё распилить. Это работает несколько иначе — сначала присмотритесь и выявите причину, по которому проект такой, какой он есть.
Расскажу вам про один реальный кейс из моей практики. Давайте пофантазируем. Допустим, у нас есть проект, над которым работают 12 команд, каждая отвечает за свою доменную область. Контролирует их frontend работу стримлид (руководитель направления), который управляет несколькими из этих команд, а в остальных просто следит за сроками.
И если вы там себе надумали, что 10+ фронтендеров в одном репозитории — это проблема, то знайте, что настоящие сложности начинаются, когда дело доходит до синхронизации, релизов и координации работы сразу 70 разработчиков — и фронтендеров и бэкендеров.

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

Это классический пример проблемного большого монолитного проекта, в котором всё взаимосвязано.
А ещё QA-команда номер 3 получает тонны задач ближе к релизу — на них сваливается работа сразу от четырёх команд, и они вынуждены не только заниматься тестированием, но и определяться, какие задачи тестировать в первую очередь.
В итоге если не большую часть времени, то половину команды тратят не на фичи, а на синхронизацию, чтобы всё не сломалось при выпуске релиза.
Попробовать уменьшить боль (но, не сильно) можно с использованием целого календаря релизов, в котором команды буквально прописывают, какие задачи войдут в состав грядущей поставки и в каких они сейчас статусах.

Каждую неделю выделяется команда релизных дежурных: бэкендер, фронтендер, PM и QA (почти как SRE команда). Дежурные занимаются только деплоем на разные стенды. Их задача — проследить, чтобы в релиз попало всё, что должно попасть (а что не должно попасть — не попало) и ничего не потерялось, а код правильно смёрджился на пред-стейдже, стейдже и продакшене.
Спойлер: всё равно постоянно что-то теряется и осыпается, потому что вчетвером невозможно уследить за кодом, который пишет сотня человек целую неделю.
Теперь заглянем в Git. Как говорится, «хрен редьки не слаще»:

На скриншоте выше — всего 20 минут рабочего времени. Коммиты идут нон-стоп в один и тот же монолитный репозиторий. Постоянные конфликты, пересечения, неожиданные правки — в общем «случилась классика».
А ещё напомню, что все это пишет несколько маленьких команд, в каждой из которых закреплены компетенции по определённым блокам. И тут начинается:

Приходит «аварийный» разработчик, открывает код и видит… сплошные комментарии.

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

Ещё раз проблемы, но кратко
Всё описанное выше усложняет не только разработку, но и управление, а также ухудшает качество продукта. Мы не можем управлять ожиданиями продактов и бизнеса, потому что постоянно возникают сюрпризы:
-
Проблемы с координацией и взаимодействием. Чем больше людей, тем сложнее согласовывать изменения. Нужно координировать задачи и зависимости между командами. Когда одна команда дорабатывает бизнес-логику, а другая — UI-компонент, и при этом обе команды о задачах друг друга не догадываются — внезапно на релизе доработанная логика либо выглядит иначе, либо вовсе не работает.
-
Сложности с поддержкой и развитием монолитного кода. В одном репозитории и одной кодовой базе неизбежно возникают merge-конфликты. Например, одна команда меняет подбор товаров, а другая добавляет телеметрию для A/B-теста. Совместить всё это можно, но это больно. Разработчикам приходится всё это держать в голове, постоянно синхронизироваться, а это высокая когнитивная нагрузка.
-
Затруднения с масштабированием разработки. Когда нужно выделить новую доменную область, вы не просто создаёте новое приложение и интегрируете его в существующее. Вам неизбежно приходится рефакторить, дробить, переделывать и переиспользовать существующий код. Невозможность независимых релизов вытекает из понятия монолита. Обновление одной части приложения неизбежно задевает все остальные.
-
Долгие и сложные релизные циклы. Календарь вы уже видели. В таких условиях абсолютно невозможно соблюдать версионность, коммититься и укладываться в сроки. В монолите любая правка затрагивает всё и сразу. Нельзя просто обновить один модуль — изменения могут потянуть за собой каскад правок по всему проекту.
-
Низкая мотивация и профессиональный рост. Разработчики чувствуют себя ничего не значащими винтиками в огромной системе. Они не могут ни на что повлиять, не видят результата своей работы и постепенно теряют мотивацию. В маленьких командах за этим проще следить.
-
Рост технического долга. Чем больше комментариев, хотфиксов и костылей — тем больше технический долг. И самое главное, если компетенция размазана по 100 разработчикам, то его почти невозможно закрывать.
-
Ограничения гибкости в использовании технологий. Если мы в одной когда-то затащили в проект Moment.js, то все остальные обречены его использовать. Менять или добавлять альтернативу — сложно либо долго. В микрофронтендах команды сами отвечают за свой стек, хотя при интеграции нужно следить за размером бандла и не забывать про tree shaking.
Для решения вышеуказанных проблем зачастую хочется прибегнуть к самому простому:

Нам кажется, что найм дополнительных людей может помочь в моменте. Но на самом деле, чем больше мы наймём, тем сильнее проблемы усугубятся в ближайшем будущем. Но есть и другой путь: микрофронтенды.
Микрофронтовая архитектура: что это и как работает
Зачем нам понадобились микрофронты
Перенесёмся в 2010-е годы. У нас есть отдельно SPA-приложение и бэкенд монолит. Фронт обращается к API по REST. Работает медленно и тяжело, но в те времена и приложения были несоизмеримо проще — их не нужно было масштабировать, и производительность была приемлемая.

Годы шли, и нагрузка на бэкэнд росла. Бэкенд начал тормозить. Появилась потребность в горизонтальном масштабировании. Пришлось делать шардирование БД и балансировку нагрузки. Так мы пришли к микросервисам.

Теперь одно большое фронтенд-приложение общается с разными бэкендовыми микросервисами. Фронт по-прежнему «лёгкий», большинство логики на бэкэнде: параллельные вызовы идут в разные API по разным доменным областям, производительность остаётся приемлемой.
Годы шли дальше, фронтенд разрастался, интерфейсы усложнялись, появлялось всё больше экранов, а логика переходила на сторону клиентского приложения. И вот мы находимся здесь:

Теперь большие фронтенд-приложения состоят из микрофронтендов, которые интегрируются между собой. Каждый из них по отдельности может общаться со своим микросервисом и производить изменения в БД, а может через микросервисы взаимодействовать с другими системами (например, документ-сервисы или вообще отдельные SAAS вроде dadata).
Особенности микрофронтов
-
Независимая разработка
Внедряя микрофронты, мы обеспечиваем независимую разработку. Так, каждый микрофронтенд может разрабатываться отдельно и (в идеале) поддерживаться отдельной автономной командой.
-
Независимый деплой
Нам больше не нужно синхронизировать релизные циклы. Если нужно каждый день деплоить небольшие фичи в быстрорастущее микрофронтовое приложение, то мы можем это делать и не зависеть от релизных циклов более крупных микрофронтов. В зависимости от типа интеграции мы будем либо пересобирать хостовое приложение, либо нужные нам микрофронты автоматически обновятся по манифесту до актуальной версии после очередной инициализации (перезагрузки вкладки браузера) на стороне пользователя.
-
Разные технологии (осторожно!)
Микрофронтендовая архитектура хоть и позволяет использовать разные технологии, но не стоит один микрофронт писать на Vue, а второй — на React. Не повторяйте это дома в проде.
Да, Single SPA и похожие решения позволяют это сделать, но лучше использовать подобную «гибкость» только для интеграции legacy-приложений. А потом всё же приводить стек к единому виду. Так мы не столкнёмся с проблемой найма разработчиков на несколько стеков сразу.
Напротив, использование разных библиотек для работы с библиотеками, датами, картами, локальной аутентификацией выглядит как хороший кейс. Каждая команда может использовать те зависимости, которые им удобны и необходимы, без необходимости увеличивать когнитивную нагрузку на соседние команды. Так код в каждом микрофронте остаётся чистым, управляемым и удобным.
-
Улучшение управления командами
Маленькими командами проще жонглировать, менять контекст, ставить какие-то фичи на паузу и управлять ожиданиями по тем, что в активной разработке. Пять человек с большей вероятностью скажут, что и когда выйдет в прод, чем 15, 20 или 100 зависящих друг от друга.
Объединяя всё вышесказанное сформулируем такой тезис:
Микрофронтовый подход помогает улучшить масштабируемость, скорость выпуска обновлений и управляемость в проектах с большим количеством фронтенд-разработчиков.
Не забывайте, что есть два варианта таких проектов: либо само приложение огромное — 30 экранов, 30 доменных областей, как Госуслуги, либо это небольшой проект, но в нём очень много разработчиков, потому что он content heavy.
Варианты интеграции микрофронтендов
Решение, как делить на микрофронты, зачастую зависит от того, проектируем мы микрофронтовую архитектуру сразу или делим существующий монолит. Как я уже говорил, есть два основных типа деления: горизонтальный и вертикальный. Эти подходы также можно комбинировать. А после того, как мы разделили микрофронты, нужно их интегрировать.
Я обычно выделяю два типа интеграции: runtime и buildtime.
Runtime-интеграция

Runtime-интеграция — это подход, при котором фронтенд-приложение собирается прямо в браузере пользователя. Пользователь обновляет страницу, а Webpack или другой бандлер загружает нужные модули по манифесту. Код разделяется на чанки и подгружается из разных CDN. Некоторые чанки могут масштабироваться горизонтально, а некоторые — нет, но приложение динамически собирается из разных источников.
Важно предусмотреть fallback (в случае отказа) поведение, чтобы приложение продолжало работать, даже если один из модулей не загрузился или находится в деплое. В этом случае можно показать заглушку или вообще его скрыть, если это, например, модуль рекомендаций.
Webpack здесь просто как пример. У него в пятой версии появился плагин Module Federation. Сейчас же все перешли на Vite, куда тоже завезли модульную федерацию.

Buildtime-интеграция
Buildtime-интеграция — это подход, при котором приложение собирается из модулей (например, npm-пакетов) и интеграция происходит во время сборки.

Для обновления любого микрофронта нужно пересобирать и деплоить хостовое приложение. При очевидных минусах (дополнительная операционная нагрузка) есть и плюс — мы точно контролируем, какой билд сейчас задеплоен на определённом стенде и что входит в его состав.
При обоих подходах к интеграции можно управлять версионностью, запускать A/B-тесты и показывать разным группам пользователей разные версии приложения с помощью манифеста. Хотя в build-time это делать проще. В package.json можно просто задать нужные версии микрофронтов. Бандлер при сборке оптимизирует код — удаляет дубликаты и выполняет tree shaking. В отличие от модульной федерации, общий код определяется ещё на этапе билда, что улучшает производительность.
Также из плюсов — проверка контрактов и типизации происходит ещё на этапе сборки, поэтому большинство ошибок интеграции удается выявить ещё до деплоя.
Если вы думаете о хранении микрофронтов в монорепозитории, то я бы не советовал, в долгосрочной перспективе это не лучший вариант. Но на этапе перехода с монолита — приемлемый компромисс. Пусть микрофронты пока лежат в монорепе, ничего страшного. Для раздельной публикации можно использовать, например, NX.
Взаимодействие микрофронтов
Однако, важнее всего понимать не как мы интегрируем микрофронты, а как они будут между собой взаимодействовать.
Здесь есть прямая аналогия с микросервисами: ключевое — их взаимодействие между собой. Это может быть синхронный или асинхронный обмен данными. Для микрофронтов лучше асинхронный вариант — через бэкенд.
Пользователь добавил товар, запрос ушел на бэкэнд и по сокетам или с помощью поллинга модуль рекомендации получил с бэкенда информацию, что в корзине появился товар новой категории. А рекомендации под это перестроились.
Альтернативный вариант — фронтовая шина событий, аналогичная Kafka или другому message-брокеру на бэке. Один микрофронт отправляет события в топик cart (корзина). Другие приложения слушают этот топик и при необходимости меняют свое поведение. Например, корректируют рекомендации исходя из того, что у нас в корзине.

Асинхронное взаимодействие хорошо тем, что если сообщение не отправилось или какой-то микрофронт его не получил, ничего не сломается. Просто модуль рекомендаций не перестроится от того, что у нас в корзине изменился товар. Пользователь даже не узнает о том, что что-то пошло не так. И это хорошо, потому что мы всё ещё соблюдаем принципы независимой разработки, независимого деплоя и независимости микрофронтендов между собой.
Общаясь через такую шину событий, нужно очень жёстко задокументировать контракт. Лучше вынести его в JSON или типизировать и сложить в отдельный npm пакет. А всем микрофронтам подгружать его и шарить между собой, чтобы ничего не сломалось. Это просто рекомендация.
Ещё не забудьте про дебаг, я бы рекомендовал отправлять кастомные события в Sentry на любые действия с шиной событий, чтобы оставался лог всех событий, а также ошибок, связанных с их отправкой.
А бонусом, для тимлидов и PM-ов, расскажу про преимущества для менеджмента.
Преимущества микрофронтендов для менеджмента
Внедрение микрофронтендовой архитектуры также влияет и на развитие людей в команде. Как только команда уменьшается, появляется место для подвига одного конкретного человека и возможности для его (этого человека) развития. Это существенно улучшает пипл-менеджмент в команде, и даже проектный менеджмент может пойти в гору.
Классический «чайка-менеджмент» (это когда PM просто приходит и спрашивает, когда код отдадут на тестирование) становится не нужен, когда у вас в команде не 15 человек, а 3. И все они точно знают, что им завтра нужно сделать. Они не могут, как Гомер Симпсон, спрятаться в кустах.

В случае микрофронтендовой архитектуры получаем:
-
Повышение автономии и ответственности
Когда у вас есть два выделенных человека, отвечающих только за этот микрофронт, увеличивается ответственность за конкретный результат. Они не зависят от других. Их не давит авторитет стримлидов, которые принимают архитектурные решения: какие библиотеки использовать и какие паттерны применять. Они могут сами брать на себя ответственность, потому что автономны. От них никто не зависит, они ни от кого не зависят. У людей появляется куда больше мотивации делать всё в срок и успевать к целям спринтов. В придачу развиваются их лидерские качества.
Напротив, когда у вас в команде 15 человек, слишком мал шанс, что кто-то из них согласится взять ответственность за всю команду. А вот когда их трое, такой человек, скорее всего, найдётся, сможет вырасти в тимлида, и всем будет хорошо.
-
Быстрое развитие технических и кросс-функциональных навыков
Появляется возможность каждому из троих попробовать новую библиотеку без ущерба для остальных 100 человек. В результате у людей в команде растут хард-скиллы, потому что они легко могут «запилотировать» что-то новое.
С точки зрения кросс-функциональных навыков, нескольким небольшим командам всё равно придётся взаимодействовать друг с другом. Но делать это будет проще, потому что количество необходимых коммуникаций уменьшается, а их качество растёт.
-
Чёткие цели и возможность карьерного роста
При составлении пресловутых ИПР (индивидуальных планов развития) вам проще выставить конкретные цели и метрики. Практически невозможно выставить KPI и метрики для приложений, где работает 100 человек. Никто их не будет выполнять — все будут думать, что кто-то из сотни точно провалит задачу. Когда вы ставите KPI для небольшой команды, скорее всего, его будут стараться выполнить, потому что это хотя бы представляется возможным.
Карьерный рост тоже становится проще. Когда человеку нужно вырасти в тимлида в команде в 15 человек, ему сложно управлять сразу таким количеством людей. Это огромная ступенька, на которую не каждый сможет забраться в одно движение. Лучше когда вы можете предложить небольшой рост шаг за шагом.
-
Улучшение командного взаимодействия и культуры
Уменьшение команды ожидаемо приводит к снижению количества конфликтов. Командный дух растёт. Чем меньше когнитивная нагрузка, тем больше и лучше вы будете общаться, делать поставки и договариваться.
-
Эффективное управление ресурсами и обучение
Этот пункт перекликается с повышением автономности, ответственности и развитием навыков. Когда у вас небольшие команды, вы можете делать маленькие корпоративные тренинги и митапы по обучению. Вам проще растить людей порционно, а не одномоментно собирать 30 человек на митап. Маленькие практические воркшопы всегда работают лучше, чем большие лекции и дискуссионные истории, на которые большинство приходит в надежде тихо отсидеться, либо не приходит вовсе.
-
Укрепление корпоративной культуры и лояльности
Когда люди видят результат, то хотят развиваться внутри компании, будь это заказная разработка, как у нас, или продуктовая. В любом случае, чем больше места для подвигов, тем чаще их будут совершать. И будут благодарны за то, что им дали такую возможность и место.
Но я вынужден добавить ложку дёгтя, потому что не бывает архитектурных паттернов без минусов, иначе бы все использовали микрофронтенды.
А минусы будут?
Успех внедрения микрофронтов зависит от многих факторов. На самом деле плюсы и минусы микрофронтов — это одно и то же, но с разных углов. Потому что плюсы — это логичное продолжение минусов.

Минусы микрофронтендов как архитектурного подхода
-
Увеличение сложности разработки
Микрофронты — это технически сложно. Нужно, чтобы Deus Ex Machina (бог из машины) пришёл, разделил ваше приложение, помог спроектировать его и сказал — теперь поддерживайте, всё будет хорошо. Такие люди должны быть на проекте, и у них должен быть определённый уровень технических компетенций. Не забываем также про сложную настройку инфраструктуры — все эти микрофронтенды ещё предстоит собирать и интегрировать.
-
Увеличение накладных расходов на запуск
В моменте вам понадобится привлечь архитекторов и DevOps инженеров. И не на два часа. Чтобы из темплейта развернуть CI/CD шаблон, потребуется детальная настройка.
-
Возможные проблемы с UX
Нет общей UI-библиотеки? Будут расхождения в дизайне. Разные версии компонентов? Инконсистентность интерфейса. Плохо настроенная runtime-интеграция? Огромный бандл и потеря производительности. Чтобы избежать хаоса, нужно продумать стратегию заранее: поддерживать единую UI-библиотеку, контролировать версии зависимостей, минимизировать дублирование кода. Так вы избежите визуального разнобоя и проблем с производительностью.
-
Управления зависимостями станет сложнее
Когда у вас один package.json — это одно. Когда их 15 (в случае с микрофронтами) — совсем другое. Микрокоманды не несут за это прямой ответственности, но всё равно сталкиваются с этой проблемой. А для стримлида сложность управления зависимостями вырастает в разы. Будьте готовы: контроль версий, обновления и совместимость станут вашей головной болью.

Поэтому, если ваша команда скорее похожа на героев фильма «Идиократия», лучше сразу не идите в микрофронты, а подрастите людей.
Кейсы, в которых микрофронты не стоит применять
-
Маленькие проекты, MVP
Можно долго проектировать и продумывать архитектуру, но за это время упустить шанс заработать денег: проект мог выстрелить, дать прибыль, получить следующую порцию инвестиций. Поэтому использовать микрофронты для MVP нужно, только если вы уже делаете 15-й проект с микрофронтами за месяц. Тогда у вас есть опыт, и всё получится быстро. Или если проект точно рассчитан на будущее, и вы уверены, что его архитектура не изменится.
Во всех остальных случаях главное — не зацикливаться на идеальном проектировании, а двигаться вперёд.
-
Отсутствие зрелой инфраструктуры
Мало раннеров для гитлаба и DevOps-ресурсов? Тогда микрофронтенды только усложнят вам жизнь.
-
Низкий уровень взаимодействия между командами
Если команды не готовы к локальной ответственности и всё держится только на сильном стримлиде, то данный подход может не прижиться. Сначала стоит подготовить заместителей на местах, перетасовать людей, наладить процессы и только потом двигаться к микрофронтам.
-
Отсутствие необходимости в независимых релизах
Если «монолитность» заложена в вашем приложении «by design» (преднамеренно, а не случайно) то разделение на микрофронты почти ничего не изменит. Вам всё равно придётся синхронизировать релизы, мэтчить версии и общаться. Не идите в микрофронты — вы потратите впустую время и деньги.
Всегда помните, что инструменты нужны для того, чтобы с их помощью решать конкретные задачи. Используйте только те инструменты, которыми умеете пользоваться, и ожидаете от них конкретного результата.
Спасибо за внимание, не забываем подраться в комментариях!
P.S. Еще раз дублирую ссылку на видео доклада с конференции.
Автор: websanya