Навигация

Микрофронтенды: композиция независимых приложений

Когда фронтенд разрастается до нескольких команд, один релизный поезд становится узким горлом: любое изменение требует согласованной сборки и деплоя всего приложения. Микрофронтенды разбивают интерфейс на независимо разрабатываемые и деплоящиеся части, которыми владеют отдельные команды. В статье разберём стратегии разбиения, способы композиции (Module Federation, Web Components, iframes, серверная композиция), изоляцию стилей и зависимостей, коммуникацию и подводные камни.

Содержание

  1. Что такое микрофронтенды
  2. Какие проблемы они решают
  3. Ключевые принципы
  4. Стратегии разбиения
  5. Build-time и runtime композиция
  6. Module Federation
  7. Web Components и Custom Elements
  8. iframe
  9. Композиция на сервере и по маршрутам
  10. Сравнение подходов
  11. Изоляция стилей
  12. Общие зависимости и version skew
  13. Коммуникация между приложениями
  14. Общая обвязка: дизайн-система, роутер, состояние
  15. Примеры реализации
  16. Проблемы и подводные камни
  17. Когда микрофронтенды не нужны
  18. Ключевые тезисы для интервью
  19. Заключение
  20. Полезные ссылки

Что такое микрофронтенды

Микрофронтенды (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 становится бутылочным горлом.
  • Технологический замок. Нельзя по частям мигрировать с одной версии или фреймворка на другой.
  • Размытая ответственность. Непонятно, кто и за что отвечает в общем коде.

Микрофронтенды дают каждой команде автономию релиза: свой пайплайн, свой деплой, свой темп. Это прямое следствие закона Конвея — структура системы отражает структуру организации; микрофронтенды сознательно приводят архитектуру в соответствие с командами.

Что микрофронтенды НЕ решают

Важно на собеседовании: это не бесплатное улучшение.

  • Они не улучшают производительность автоматически — часто её ухудшают (лишние запросы, дублирование зависимостей).
  • Они не устраняют необходимость в дизайн-системе и общих контрактах — наоборот, повышают требования к ним.
  • Они не заменяют грамотное модульное разбиение внутри команды.

Ключевые принципы

  1. Независимый деплой. Каждый remote собирается и выкатывается сам, без пересборки shell.
  2. Автономия команд. Команда владеет фичей end-to-end: UI, логика, данные, тесты.
  3. Технологическая независимость (опционально). Разные remotes могут использовать разные фреймворки и версии.
  4. Слабые контракты. Общение только через явный API: props, события, URL. Никаких импортов чужих внутренностей.
  5. Изоляция. Ошибка или падение одного 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 больше не совпадают.

Практики управления:

  1. shared с singleton и requiredVersion — общая библиотека существует в одной копии и с проверкой совместимости.
  2. Обратная совместимость контрактов. Exposed-API remote версионируется: смена сигнатуры — новая версия, старая поддерживается один релизный цикл.
  3. Постепенные мажорные апгрейды. Сначала shared-зависимость обновили в shell, потом по очереди в remotes.
  4. 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, договаривайтесь о дизайн-системе и обратно совместимых контрактах. И помните: если у вас одна команда и единый релизный цикл — микрофронтенды принесут больше вреда, чем пользы.

Полезные ссылки