Микрофронтенды: композиция независимых приложений
Когда фронтенд разрастается до нескольких команд, один релизный поезд становится узким горлом: любое изменение требует согласованной сборки и деплоя всего приложения. Микрофронтенды разбивают интерфейс на независимо разрабатываемые и деплоящиеся части, которыми владеют отдельные команды. В статье разберём стратегии разбиения, способы композиции (Module Federation, Web Components, iframes, серверная композиция), изоляцию стилей и зависимостей, коммуникацию и подводные камни.
Содержание
- Что такое микрофронтенды
- Какие проблемы они решают
- Ключевые принципы
- Стратегии разбиения
- Build-time и runtime композиция
- Module Federation
- Web Components и Custom Elements
- iframe
- Композиция на сервере и по маршрутам
- Сравнение подходов
- Изоляция стилей
- Общие зависимости и version skew
- Коммуникация между приложениями
- Общая обвязка: дизайн-система, роутер, состояние
- Примеры реализации
- Проблемы и подводные камни
- Когда микрофронтенды не нужны
- Ключевые тезисы для интервью
- Заключение
- Полезные ссылки
Что такое микрофронтенды
Микрофронтенды (micro-frontends) — это архитектурный подход, при котором фронтенд-приложение разбивается на несколько самостоятельных приложений (remote, micro-app, fragment), каждое из которых:
- разрабатывается отдельной командой;
- собирается и деплоится независимо;
- владеет своей частью UI и бизнес-логики;
- интегрируется в общий интерфейс через формальный контракт.
Название — прямая аналогия с микросервисами: если backend разбивают на сервисы по домену, то фронтенд разбивают на микрофронтенды. Пользователь видит единый интерфейс и не знает, что за ним стоит несколько независимых приложений.
Ключевое отличие от монолитного SPA и от монорепозитория: монорепозиторий — это про один репозиторий и одну сборку, а микрофронтенды — про независимые сборки и деплои. Их часто путают, но это ортогональные оси: можно иметь микрофронтенды в одном монорепозитории (Turborepo/Nx) или в разных репозиториях.
┌──────────────────────────────────────────────────────────┐
│ Shell / Host (контейнер) │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ Каталог │ │ Корзина │ │ Профиль │ │
│ │ (team A) │ │ (team B) │ │ (team C) │ │
│ │ React │ │ Vue │ │ React │ │
│ └────────────┘ └────────────┘ └────────────┘ │
│ remote A remote B remote C │
└──────────────────────────────────────────────────────────┘
Какие проблемы они решают
Проблема монолитного фронтенда
- Один релизный поезд. Изменение в одном месте требует пересборки и деплоя всего приложения, даже если менялся один экран.
- Блокировка командами. Команды ждут друг друга: пока не смержится чужая фича, релиз не уезжает.
- Разрастание сборки. Сборка монолита со временем занимает десятки минут, CI становится бутылочным горлом.
- Технологический замок. Нельзя по частям мигрировать с одной версии или фреймворка на другой.
- Размытая ответственность. Непонятно, кто и за что отвечает в общем коде.
Микрофронтенды дают каждой команде автономию релиза: свой пайплайн, свой деплой, свой темп. Это прямое следствие закона Конвея — структура системы отражает структуру организации; микрофронтенды сознательно приводят архитектуру в соответствие с командами.
Что микрофронтенды НЕ решают
Важно на собеседовании: это не бесплатное улучшение.
- Они не улучшают производительность автоматически — часто её ухудшают (лишние запросы, дублирование зависимостей).
- Они не устраняют необходимость в дизайн-системе и общих контрактах — наоборот, повышают требования к ним.
- Они не заменяют грамотное модульное разбиение внутри команды.
Ключевые принципы
- Независимый деплой. Каждый remote собирается и выкатывается сам, без пересборки shell.
- Автономия команд. Команда владеет фичей end-to-end: UI, логика, данные, тесты.
- Технологическая независимость (опционально). Разные remotes могут использовать разные фреймворки и версии.
- Слабые контракты. Общение только через явный API: props, события, URL. Никаких импортов чужих внутренностей.
- Изоляция. Ошибка или падение одного remote не должно ронять весь контейнер.
Стратегии разбиения
Вертикальное разбиение (по домену)
Каждый микрофронтенд — это бизнес-домен: каталог, оформление заказа, личный кабинет, админка. Команда владеет доменом целиком.
- ✅ Понятные границы и высокая автономия
- ✅ Изменения локализованы в одном домене
- ❌ Сложнее с общими частями (шапка, навигация) — их выносят в shell или в отдельный shared-слой
Горизонтальное разбиение (по слоям)
Деление по техническим слоям: отдельный микрофронтенд для header, для footer, для общего стора.
- ❌ Практически всегда антипаттерн: изменение общего слоя снова блокирует всех
- ❌ Высокая связанность между «слоями»
Правило: разбивайте вертикально по домену. Горизонтальное разбиение воспроизводит проблемы монолита, но с накладными расходами распределённой системы.
Build-time и runtime композиция
Ключевая развилка — когда части собираются в одно приложение.
Build-time (на этапе сборки)
Remote-пакеты подключаются как обычные npm-зависимости (@company/catalog), и shell собирает всё в один бандл.
- ✅ Простая и быстрая загрузка, полная типизация, один деплой
- ❌ Изменение remote требует пересборки и деплоя shell → теряется независимость деплоя
- Когда: команды делят код, но релизят вместе; фактически это модульный монолит
Runtime (в браузере)
Shell загружает remote в момент исполнения по сети, обычно лениво и по требованию.
- ✅ Настоящий независимый деплой: remote выкатывается сам
- ✅ Ленивая загрузка: код грузится только когда нужен
- ❌ Сетевые задержки, сложнее типизация, версионирование, обработка ошибок загрузки
- Когда: нужна реальная автономия релизов
Итог: независимый деплой достижим только при runtime-композиции. Build-time композиция — это способ переиспользовать код, а не организовать микрофронтенды.
Module Federation
Module Federation (появился в Webpack 5, поддерживается Rspack, есть плагины для Vite) — самый популярный механизм runtime-композиции. Идея: сборки обмениваются модулями в рантайме, без публикации в npm.
Термины:
- host (shell) — контейнер, который загружает чужие модули;
- remote — приложение, которое exposes (отдаёт) свои модули;
remoteEntry.js— манифест remote с метаданными о том, что и как он предоставляет;shared— общие зависимости, которые host и remote соглашаются не дублировать.
Remote: что отдаём
// webpack.config.js remote-приложения "catalog"
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'catalog',
filename: 'remoteEntry.js',
exposes: {
'./ProductList': './src/ProductList',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
};
Host: как подключаем
// webpack.config.js shell-приложения
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'shell',
remotes: {
catalog: 'catalog@https://catalog.example.com/remoteEntry.js',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
}),
],
};
// Загрузка remote-модуля лениво, с fallback и обработкой ошибки
const ProductList = React.lazy(() => import('catalog/ProductList'));
function CatalogPage() {
return (
<React.Suspense fallback={<Spinner />}>
<ProductList />
</React.Suspense>
);
}
Что даёт shared
shared — сердце корректной работы. Без него host и remote притащат две копии React: два экземпляра библиотеки означают два разных контекста и две разные реализации хуков.
shared: {
react: {
singleton: true, // ровно один экземпляр в рантайме
requiredVersion: '^18.0.0',
strictVersion: true, // падать, а не молча брать несовместимую версию
},
}
singleton: true— гарантирует одну копию (критично для React, Vue, стора);requiredVersion— semver-диапазон, который ждёт потребитель;strictVersion— жёсткая проверка совместимости вместо тихого fallback.
Native Federation
@softarc/native-federation переносит идею Module Federation на стандартные механизмы браузера (ESM + import maps), чтобы не привязываться к Webpack. Rspack и Vite имеют свои федеративные плагины (@module-federation/vite, @originjs/vite-plugin-federation). Смысл один: runtime-загрузка exposed-модулей.
Web Components и Custom Elements
Web Components — набор браузерных стандартов (custom elements, Shadow DOM, HTML templates), не зависящий от фреймворка. Remote оборачивает своё приложение в кастомный элемент, а shell просто вставляет тег.
<!-- Shell ничего не знает о фреймворке внутри -->
<product-catalog api-base="/api"></product-catalog>
Vue умеет отдавать компонент как custom element «из коробки»:
// Vue: defineCustomElement превращает SFC в custom element
import { defineCustomElement } from 'vue';
import Catalog from './Catalog.ce.vue';
customElements.define('product-catalog', defineCustomElement(Catalog));
React официально не компилирует компонент в custom element, поэтому используют обёртки (react-to-webcomponent) или рендер в Shadow DOM через createRoot.
Сильные стороны:
- фреймворк-агностичный контракт: host не знает, что внутри;
- Shadow DOM изолирует стили;
- можно смешивать React, Vue, Svelte в одном приложении.
Ограничения:
- Shadow DOM изолирует не всё: CSS custom properties проникают внутрь, а глобальные стили хоста — нет, поэтому тему/токены приходится прокидывать осознанно;
- события компонента не всегда всплывают насквозь (пересекая shadow boundary, их переоформляют в
CustomEvent); - React и Vue внутри custom element всё равно требуют своей копии рантайма — проблему общих зависимостей это не решает.
iframe
iframe — самый простой и самый жёсткий способ изоляции: каждый микрофронтенд загружается как отдельный документ.
- ✅ Полная изоляция: JS, CSS, DOM, история, версии библиотек не пересекаются
- ✅ Идеально для недоверенного кода и сторонних виджетов
- ❌ Общение только через
postMessage(асинхронно, без типов) - ❌ Нельзя разделить единый URL и history — deep-linking и back/forward ломаются
- ❌ Высота подстраивается только вручную (нужен ResizeObserver), скролл и события мыши ведут себя иначе
- ❌ Тяжелее по памяти и загрузке (отдельный документ на каждый iframe)
// Shell → iframe
iframe.contentWindow.postMessage({ type: 'SET_USER', user }, 'https://widget.example.com');
// iframe → shell
window.addEventListener('message', (event) => {
if (event.origin !== 'https://shell.example.com') return;
// обработать событие
});
Когда оправдан: чужие вендоры, платежные формы, sandbox-превью, различие в стеке настолько принципиальное, что изоляция важнее UX. Как основной способ композиции собственного приложения — антипаттерн.
Композиция на сервере и по маршрутам
Композиция по маршрутам (реверс-прокси)
Простейший рабочий вариант: каждый микрофронтенд отвечает за свой набор путей, а реверс-прокси (nginx, CDN) маршрутизирует /catalog/* на одно приложение, /cart/* — на другое. Пользователь перемещается между ними по ссылкам (полная перезагрузка или MPA).
- ✅ Максимальная изоляция и простота инфраструктуры, отличный для SSR/MPA
- ❌ Переходы между приложениями не SPA-шные (перезагрузка), теряется общий рантайм
- Решение плавных переходов — связка с runtime-композицией или client-side router, знающий о чужих маршрутах
Серверная композиция (SSR-федерация)
Shell рендерит страницу на сервере и встраивает HTML/потоки других приложений на этапе ответа: Edge Side Includes (ESI), стриминг с Suspense, SSR-режим Module Federation, серверные компоненты.
- ✅ Быстрый FCP, SEO, композиция без клиентского JS-склеивания
- ❌ Сложная инфраструктура, версионирование серверных контрактов, координация рантаймов
- Когда: критичны SEO и производительность первого рендера
Сравнение подходов
| Подход | Независимый деплой | Изоляция | Общий рантайм | Смешение фреймворков | Сложность |
|---|---|---|---|---|---|
| Build-time (npm-пакеты) | ❌ (нужна пересборка shell) | Низкая | Да (один бандл) | Ограниченно | Низкая |
| Module Federation | ✅ | Средняя | Да (shared) | Да | Высокая |
| Web Components | ✅ | Высокая (Shadow DOM) | Ограниченно | Да | Средняя |
| iframe | ✅ | Максимальная | Нет | Да (полностью) | Низкая |
| Реверс-прокси / маршруты | ✅ | Максимальная | Нет | Да | Низкая |
| Серверная композиция | ✅ | Высокая | Нет (HTML) | Да | Очень высокая |
Выбора «лучшего» нет — есть соответствие задаче: от модульного монолита (build-time) до полной изоляции (iframe, реверс-прокси) и до богатого общего рантайма (Module Federation).
Изоляция стилей
Главная боль runtime-композиции: CSS глобален, и remotes переопределяют друг друга.
- CSS Modules / scoped CSS / CSS-in-JS — изоляция внутри каждого remote (хорошая база, но не защищает от глобальных селекторов и
!important). - Shadow DOM — реальная граница: внутренние и внешние стили не пересекаются, кроме custom properties. Это главный аргумент за Web Components для композиции.
- Префиксы и
@layer— договорённость: каждый remote пишет стили в свой слой каскада, слои объявлены в общем порядке.
/* Каждый remote в своём слое — предсказуемый приоритет без войн специфичности */
@layer shell, catalog, cart, profile;
@layer catalog {
.card { /* стили каталога */ }
}
Префиксация классов (catalog__card), единые токены дизайн-системы и явный порядок слоёв — обязательная договорённость между командами.
Общие зависимости и version skew
Version skew — рассинхронизация версий между независимо задеплоенными приложениями. Пример: shell собрался с новой мажорной версией общей UI-библиотеки, а remote ещё на старой, и props больше не совпадают.
Практики управления:
sharedсsingletonиrequiredVersion— общая библиотека существует в одной копии и с проверкой совместимости.- Обратная совместимость контрактов. Exposed-API remote версионируется: смена сигнатуры — новая версия, старая поддерживается один релизный цикл.
- Постепенные мажорные апгрейды. Сначала shared-зависимость обновили в shell, потом по очереди в remotes.
strictVersion: falseтам, где нужна деградация вместо падения, иtrue— где важнее не допустить несовместимость.
Отдельно: две копии React/Vue ломают приложение, поэтому фреймворк всегда в shared со singleton: true.
Коммуникация между приложениями
Основное правило — слабые контракты, чтобы не создать распределённый монолит.
- URL и history API — самый надёжный канал: shell и remotes читают состояние из адреса, deep-linking работает естественно.
- CustomEvent на
window— простой event bus без общей зависимости.
// Remote B сообщает о добавлении товара
window.dispatchEvent(new CustomEvent('cart:add', { detail: { id: 42 } }));
// Remote A подписывается
window.addEventListener('cart:add', (e) => updateBadge(e.detail.id));
- Явно экспортированный модуль с версионированным API (shared store, event bus как пакет) — когда нужна типизация.
postMessage— только если граница проходит через iframe.
Чего избегать: прямых импортов внутренних путей чужого приложения и неявного общего глобального состояния (кроме фреймворка и дизайн-системы).
Общая обвязка: дизайн-система, роутер, состояние
- Дизайн-система — единственная обязательная общая часть: компоненты и токены либо дублируются (плохо), либо живут в отдельном пакете с semver (обычно).
- Роутер — shell владеет роутингом и передаёт remote его сегмент пути; remote не должен считать, что управляет адресом целиком.
- Состояние — держите его внутри remotes. Общее глобальное состояние между микрофронтендами быстро превращает их обратно в монолит; для кросс-доменной синхронизации используйте события или URL, а не общий стор.
- Аутентификация и тема — на уровне shell: он передаёт токен, локаль, тему через контракт (props, custom properties, события).
Примеры реализации
Vue remote через Vite Federation
// vite.config.js remote-приложения на Vue
import vue from '@vitejs/plugin-vue';
import federation from '@originjs/vite-plugin-federation';
export default {
plugins: [
vue(),
federation({
name: 'cart',
filename: 'remoteEntry.js',
exposes: { './CartWidget': './src/CartWidget.vue' },
shared: { vue: { singleton: true, requiredVersion: '^3.0.0' } },
}),
],
};
<!-- Shell на Vue: подключает remote как асинхронный компонент -->
<script setup>
import { defineAsyncComponent } from 'vue';
const CartWidget = defineAsyncComponent(() => import('cart/CartWidget'));
</script>
<template>
<Suspense>
<CartWidget />
</Suspense>
</template>
React host + fallback при недоступности remote
const Catalog = React.lazy(() => import('catalog/ProductList'));
function SafeRemote({ children }) {
return (
<ErrorBoundary fallback={<p>Модуль временно недоступен</p>}>
<React.Suspense fallback={<Spinner />}>{children}</React.Suspense>
</ErrorBoundary>
);
}
ErrorBoundary вокруг ленивой загрузки критичен: если remoteEntry.js не загрузился, падать должен только один виджет, а не весь shell.
Проблемы и подводные камни
- Дублирование зависимостей. Без корректного
sharedв бандл попадают две копии библиотек → раздутый размер и две копии React. - Version skew. Независимые деплои означают, что стороны рассинхронизированы; контракты должны быть обратно совместимы.
- Производительность. Лишние сетевые запросы, несколько рантаймов фреймворков, отсутствие общего кэша чанков → медленнее монолита при неаккуратной настройке.
- Сложность DX и отладки. Локальная разработка требует поднять несколько приложений; отладка распределена по разным сборкам и source maps.
- Тестирование. Растёт роль контрактных тестов и E2E: юнит-тестами отдельного remote интеграция не проверяется.
- Observability. Ошибки и метрики нужно агрегировать по всем remotes, иначе инциденты невидимы.
- Связанность через общий стор. Попытка шарить состояние «чтобы было проще» убивает автономию и снова создаёт монолит.
- Дизайн-система как обязательство. Без общей дизайн-системы композиция превращается в лоскутное одеяло.
Когда микрофронтенды не нужны
- Одна команда. Нет организационной причины для разбиения — только накладные расходы.
- Небольшое приложение. Модульного монолита (feature-sliced, монорепозиторий) более чем достаточно.
- Единый релизный цикл. Если всё равно релизите вместе, независимый деплой не даёт выгоды.
- Стартап на ранней стадии. Архитектура под масштаб команды, которой ещё нет, — преждевременная оптимизация.
Хороший вопрос на собеседовании — не «умеете ли вы в микрофронтенды», а «понимаете ли вы, когда они вредны». Разбиение оправдано, когда есть несколько команд с реально независимыми релизами и потребность в автономии.
Ключевые тезисы для интервью
- Микрофронтенды — независимо разрабатываемые и деплоящиеся части фронтенда, владеемые отдельными командами; аналог микросервисов на клиенте.
- Независимый деплой возможен только при runtime-композиции; build-time композиция — это переиспользование кода, а не автономия.
- Разбивайте вертикально по бизнес-домену; горизонтальное деление по слоям воспроизводит проблемы монолита.
- Module Federation обменивается модулями в рантайме через
remoteEntry.js; host указывает remotes в конфиге, remote exposes свои модули. sharedсоsingleton: trueпредотвращает две копии React/Vue, которые ломают контекст и хуки.- Web Components дают фреймворк-агностичный контракт и изоляцию через Shadow DOM, но CSS custom properties проникают сквозь границу.
- iframe — максимальная изоляция, но ломает единый URL/history и требует ручной подгонки высоты; оправдан для чужого кода.
- Version skew — рассинхронизация версий между независимыми деплоями; лечится semver-диапазонами в
shared,strictVersionи обратной совместимостью контрактов. - Обмен данными — через URL, CustomEvent и версионированные экспортированные модули, а не через общий глобальный стор.
- Микрофронтенды избыточны для одной команды и небольшого приложения; они решают организационную проблему, а не техническую.
Заключение
Микрофронтенды — это организационное решение, выраженное в архитектуре: они позволяют нескольким командам независимо разрабатывать и деплоить части одного интерфейса. Платят за это runtime-связанностью, version skew, сложностью отладки и тестирования. Ключевые инструменты — Module Federation для runtime-композиции с общими зависимости через shared, Web Components для фреймворк-агностичного контракта и Shadow DOM-изоляции, iframe и реверс-прокси для максимальной изоляции. Разбивайте вертикально по домену, держите состояние внутри remotes, договаривайтесь о дизайн-системе и обратно совместимых контрактах. И помните: если у вас одна команда и единый релизный цикл — микрофронтенды принесут больше вреда, чем пользы.