Сегодня я расскажу про кейс переезда, который до сих пор продолжается, — переезда Яндекс Еды на микрофронтендовую технологию. Расскажу, зачем мы это сделали, почему у нас это получилось и что помогает нам делать этот процесс гораздо быстрее.
Как работают эксперименты и проверка гипотез
Про то, как работают эксперименты и проверка гипотез, расскажу чуть позднее.
Как правило, у бигтехов есть конфигуратор, некая «экспериментница» — система, которая позволяет раскатить определённый функционал на часть пользователей. Обычно это фича-флаги.
Такая система работает на всех фронтовых и бэкенд-платформах — неважно, о каком именно компоненте мы говорим.
Например, мы можем раскатить фичу на 50 или 30% пользователей либо выбрать аудиторию по определённым условиям. Это позволяет гибко конфигурировать приложение в зависимости от обстоятельств.
Почему мы пришли к микрофронтендам
Почему вообще мы пришли к микрофронтендам и зачем это оказалось нужно?
Одна из причин связана с международным направлением. В некоторых странах, где представлена Яндекс Еда, мобильный интернет работает нестабильно, а наше приложение достаточно тяжёлое. Грузить его в таких условиях действительно больно.
Мы даже ездили на места и пытались там оптимизировать приложение.
В какой-то момент я выступал на фронтенд-конференции, где ребята из Мегамаркета рассказывали, как они используют микрофронтенды в разных продуктах. Мне показалось, что это в целом может быть хорошим решением для Яндекса.
Я принёс эту идею в команду платформы, и мы постепенно начали её реализовывать.
Тогда AI ещё не была настолько популярной и не могла выполнять тот объём задач, который умеет выполнять сейчас.
Что даёт микрофронтенд помимо переиспользования
У нас крупный продукт. Релизы происходят раз в неделю, и релизная ветка тоже отводится раз в неделю.
Если каким-то образом в проде оказывается что-то очень плохое, нам приходится откатывать всю функциональность, которая была отведена за эту неделю. Это могут быть срочные фичи и другие изменения.
Сам откат тоже занимает определённое время.
Вот небольшое сравнение того, как происходит релиз в монолите и как — в микрофронтенде.
-
Частота релизов: в монолите — раз в неделю, в микрофронте — практически когда угодно.
-
Длительность выхода: в монолите — около двух часов, потому что происходит полный раскат примерно на 90 подов; в микрофронте — фактически загрузка файлов в S3-хранилище.
-
Зона поражения при ошибке: в монолите может деградировать вся функциональность, отведённая в рамках релиза; в микрофронте зона ответственности значительно уже.
-
Откат: в монолите занимает около двух часов вместе с откатом всех этих 90 подов; в микрофронте — буквально по щелчку пальца.
-
Регресс и код-фриз: при релизе монолита дежурные QA-инженеры проверяют большой объём функциональности с каждым отводом. В микрофронте достаточно ответственной команде проверить конкретную изменённую часть, после чего она может спокойно отправляться в прод.
Как устроена работа с микрофронтами
Базово у нас есть три части: монолит, JavaScript микрофронта и типы, которые мы продолжаем интегрировать.
Как мы использовали систему экспериментов и конфигураций, чтобы сделать всё это достаточно красиво?
Во время публикации микрофронта мы буквально загружаем его в S3-хранилище со ссылкой на файл. Ссылка представляет собой название микрофронта и версию.
S3 может хранить большое количество версий — фактически сколько угодно.
Когда CI сообщает, что микрофронт зарелизился, автоматика сама создаёт драфт на изменение технического конфига. Он буквально меняет версию.
И вот здесь становится понятен тот самый мгновенный откат, о котором я говорил.
Если что-то пошло не так, SRE-инженер заходит в конфиг, меняет версию обратно — и старая версия микрофронта снова на месте. Всё происходит практически моментально.
А все типы и другие сущности, которые должны использоваться за пределами микрофронта — например, другими микрофронтами или монолитом, — публикуются в npm-пакет и устанавливаются как зависимость.
Работа с версионностью
Вот небольшой пример того, как у нас происходит работа с версионностью.
Мы подтягиваем эксперимент для конкретного микрофронта, конфигурацию, версию фронта и кешируем полученное значение.
Если значение уже закешировано, отдаём его.
Мы считаем, что во время работы с приложением необязательно обновлять версию. После полной перезагрузки закешированных значений уже не будет.
Дальше мы достаём модуль из контейнера, кастим его и импортируем через технологию Module Federation.
Наружу также выплёвывается регистр, если этот микрофронт успешно подтянулся.
Проброс общих частей
Теперь о том, как мы пробрасываем в микрофронтенд общие части.
У нас, например, темы, эксперименты и другие сущности хранятся в монолите. В микрофронтенд мы передаём только узкую часть сервисов, которая нужна именно ему.
Вот пример такого набора сервисов: темы, конфигурация, переводы, хуки, аналитика, диплинк-менеджеры и другие компоненты.
Это пример того, как сервис двигается из монолита и затем используется для проброса нужной функциональности в микрофронты.
Масштаб сегодня
Сегодня у нас около 20 микрофронтов в проде.
Но это на самом деле не так много, учитывая, насколько огромная Яндекс Еда в целом.
Кроме того, что мы можем выделить определённую зону в отдельный микрофронт, мы можем указать отдельные entry point.
Например, есть модалка адресов, которая используется во всём приложении. Она лежит отдельным entry point.
То есть микрофронт находится в одном репозитории, но entry point для конкретной модалки вынесен отдельно, и его можно включать независимо.
Сейчас у нас два основных репозитория: монолит и репозиторий, который мы называем «фастфудом» — потому что он работает быстрее.
Ну и остался план-хвост монолита, который постепенно худеет.
Переезд: с чего начинается
Самая скучная часть начинается с переезда.
Шаги у нас практически всегда одинаковые. Мы разделили процесс на два этапа, выделили повторяющиеся действия и автоматизировали их.
Что происходит на первом этапе?
-
Создаётся каркас микрофронта — это делает небольшой скрипт.
-
Создаётся эксперимент в «экспериментнице», о которой я говорил.
-
Создаётся scope типов.
-
Создаётся свитчер на уровне монолита, который по эксперименту решает, откуда брать функциональность: из монолита или из микрофронта.
-
Добавляются роуты, npm-зависимости, спецификации экспериментов, переводы и другие необходимые сущности.
В результате получаем два пул-реквеста в два репозитория: фастфуд и монолит.
После первого этапа у нас есть микрофронт, который пока содержит заглушку. Есть свитчер, который на уровне монолита позволяет переключаться между монолитной и микрофронтовой реализацией.
Есть необходимые типы и инфраструктура экспериментов, которая позволяет по щелчку переключать версии.
Автоматизация первого этапа
Теперь расскажу, как мы автоматизировали первый этап и как на втором этапе нам продолжают помогать нейронки.
Для первого этапа у нас есть скилл — генерация микрофронта.
Что он делает?
Он генерирует каркас с дефолтными файлами: tsconfig и другими необходимыми сущностями. По сути, просто создаёт заготовку, в которую дальше можно писать код.
Затем автоматически через MCP создаётся эксперимент в наших конфигураторах.
После этого внутри микрофронта создаются общие типы, а на уровне монолита появляется переключатель, который смотрит на конфиг и выбирает, откуда брать функциональность — из микрофронта или из монолита.
Также загружаются первоначальные дефолтные роуты и генерируются эксперименты для соответствующей конфигурации.
Теперь у каждого микрофронта есть только те эксперименты, которые нужны именно ему.
Раньше все эксперименты находились в монолите. Теперь каждый микрофронт инкапсулирует нужную ему часть и подсасывает только необходимые эксперименты.
По результатам работы скилла буквально создаются два пул-реквеста:
-
один — в фастфуд, с новым микрофронтом;
-
второй — в монолит, со свитчером и пробросами некоторых сервисов.
Человеку остаётся оформить созданный эксперимент и завести ключи в Бункере.
Бункер — это наша внутренняя технология, которая позволяет использовать переводы для конкретного интерфейса.
Этап два: куда уносить код
Второй этап уже гораздо менее тривиальный и от раза к разу меняется.
Нужно определить границы модуля: какие зависимости у него есть, какие компоненты используются, какие данные нужны для хранилища и так далее.
Переиспользовать всё подряд не всегда получается. Но мы стараемся использовать то, что уже выносилось в другие микрофронты, и не дублировать код.
Компонент из монолита может тянуть за собой несколько зависимостей.
Например, общие компоненты, хуки, типы для инпутов, useScope, useDispose и другие сущности.
Некоторые из них можно вынести во внешнее общее пространство, после чего их смогут использовать другие микрофронты.
Таким образом, наша задача — проанализировать зависимости и понять:
-
что можно вынести в общее место;
-
что уже вынес кто-то до нас и что можно переиспользовать;
-
какие сущности должны остаться внутри конкретного микрофронта;
-
какие типы и зависимости нужно предоставить наружу для других микрофронтов и прод-приложения.
Как помогает AI на втором этапе
Вот здесь нам и помогает AI.
Она буквально смотрит на весь репозиторий, анализирует существующие микрофронты и лучшие практики.
Дальше она может что-то импортировать, создать необходимые сущности и вынести компоненты, которые нигде не используются, в папку shared.
То, что находится в shared, публикуется во внутренний npm и затем может использоваться другими микрофронтами или прод-приложением.
В результате работы AI мы получаем своеобразную карту зависимостей.
Как правило, мы дополнительно используем что-нибудь вроде progress.md, чтобы нейронка могла шаг за шагом фиксировать, что она делает.
Одним промптом вынести весь клубок зависимостей очень сложно. У модели ограниченный контекст, поэтому мы складываем прогресс в файл. Она может в любой момент открыть его, прочитать и понять, на каком этапе находится.
В итоге получаем карту зависимостей, черновой импорт, перенос UI и импортов, адаптацию стилей, подготовку entry point и другие необходимые изменения.
Это скорее план работы.
При этом AI смотрит, что уже сделали соседние микрофронты: что можно переиспользовать, а что не нужно копировать.
Скилл для выноса общего: виджеты
Одна из наших зон ответственности — выносить то, что является общим.
У нас есть такая история, как виджеты. Их на самом деле десятки.
Виджеты — это то, что вы видите, когда заходите в Яндекс Еду на главную: поиск, выдача, фильтры и другие элементы.
Они конфигурируемые и имеют примерно одинаковую структуру зависимостей и работы.
Ребята из команды выбора сделали скилл, который буквально автоматизирует перенос всех этих виджетов.
Человеку остаётся отревьюить изменения, сделать небольшой diff-тест на стенде и отдать результат QA.
И в целом всё.
Это хороший пример того, как можно использовать AI: задача достаточно типовая, структура понятна, а большая часть работы повторяется из раза в раз.
На какие грабли мы натыкаемся
Конечно, без проблем не обходится.
Импорт бандла соседа
У нас стоят жёсткие ограничения на то, чтобы один микрофронтенд импортировал другой именно JavaScript-частью.
Монолит выступает мостом для общения между микрофронтами.
Мы специально ограничили прямое взаимодействие, чтобы один микрофронт не начинал зависеть от другого.
Дублинг компонентов
Это как раз то, о чём я говорил выше.
Когда несколько команд параллельно выносят свои зоны ответственности, может случиться так, что в разных микрофронтах появляются три одинаковых компонента.
Каждый из них нужен нескольким командам, но вместо общего компонента появляется несколько копий.
Сейчас это происходит, и мы с этим аккуратно боремся.
Сторы не переезжают
Как бы хорошо мы ни хотели инкапсулировать отдельную зону ответственности, сторы у нас зашиты в монолитной части приложения.
Однако мы сделали отдельный MFStore, который инкапсулирует модели данных.
Он описывает интерфейс этих моделей, а сами модели создаются непосредственно в монолите.
Дальше монолит через сервисы пробрасывает их в соответствующие микрофронты.
Там происходит взаимодействие с моделями, вызовы методов, изменение и наполнение стора.
После этого монолит передаёт обновлённые данные остальным микрофронтам. Всё это реализовано через подписки.
Чистка экспериментов
Последний скилл в этом процессе — чистка прямых экспериментов.
Когда мы понимаем, что функциональность уже стабильно живёт в микрофронте и больше не нужна в монолите, эксперимент и свитчер можно удалить.
Мы просто говорим:
«Всё, нам больше не нужен этот эксперимент. Почисти всё и теперь всегда используй данные из микрофронта».
AI удаляет свитчер и связанные с ним изменения.
Это не такая большая работа, чтобы делать её вручную. Но это типичная рутина, которая повторяется из раза в раз, поэтому её имеет смысл автоматизировать.
Что остаётся человеку
В итоге за агентом остаются:
Человеку остаётся определить, что и когда переезжает, а также провести качественное код-ревью того, что делает нейронка.
Это очень важно.
Путь к абсолютному доверию к нейронке — это путь к багу в коде каждый день.
Кроме того, за человеком остаются публикация экспериментов, обновление версий, схем конфигов и переводов в Бункере, а также определение архитектурных границ микрофронта.
Именно здесь человек по-прежнему принимает ключевые решения, а AI берёт на себя повторяющуюся техническую работу.