Наш фронтенд начинался как простой SPA на React, собранный с помощью Vite — типичный монолит с несколькими страницами. Со временем проект оброс новыми функциями и интеграциями и начал становиться всё сложнее в поддержке.
На горизонте появились новые вызовы: к продукту планировалось подключать всё больше независимых сервисов, а значит — ещё больше интеграций и роста кодовой базы. Мы понимали, что нагрузка на инфраструктуру будет только увеличиваться, поэтому решили заранее заложить архитектуру с расчётом на масштабирование.
После изучения разных вариантов мы остановились на подходе микрофронтендов. Хотелось разграничить зоны ответственности между командами и ускорить разработку, не теряя гибкости. В качестве сборщика решили остаться на Vite — он быстро развивался, предлагал отличную DX и поддержку модульной федерации через плагин. Кроме того, важно было сохранить единый репозиторий, чтобы упростить CI/CD и управление зависимостями.

Немного базы
Подход module federation оправдан, когда приложение становится слишком большим, начинает требовать независимого релиза разных частей и усложняется в сопровождении. Также стоит понимать, что микрофронты — это не только про разбиение кода, но и про инфраструктурные затраты: настройка CI/CD, изоляция окружений, передача состояния, роутинг и, зачастую, потеря некоторых плюсов «единого SPA».
Перед внедрением мы детально проанализировали потенциальные риски: дублирование зависимостей, рост сложности конфигурации, ухудшение DX при локальной разработке (и возможные костыли для улучшения DX), увеличение порога входа в кодовую базу. Взвесив их на фоне ожидаемой пользы — масштабируемости, независимой разработки, упрощения интеграции новых команд — мы пришли к выводу, что плюсы в нашем случае перевешивают.
Что выбрали для реализации
Так как мы остались на Vite, то использовали активно развивающийся Vite plugin for Module Federation. Его настройка похожа на Webpack Module Federation, хотя на июнь 2025 года у плагина есть свои ограничения, о которых расскажу ниже. Разработку мы начали в тестовом репозитории: проверили работу и в dev-режиме, и в сборке.
Базовая конфигурация host-контейнера и модулей
Пример репозитория, в котором показан переход от монолита к микрофронтендам с использованием Vite и @module-federation/vite. Пример максимально приближен к реальному проекту:
используется общая конфигурация через monorepo,
настроена локальная разработка,
реализована передача состояния и роутинг между микрофронтами.
Репозиторий можно использовать как отправную точку для собственных экспериментов или пилотных внедрений.
Основная часть переезда — это подготовка vite-конфигов для сервисов.
Базовый конфиг для host-контейнера:
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { federation } from '@module-federation/vite';
export default defineConfig({
server: {
port: 5173,
origin: 'http://localhost:5173',
},
base: 'http://localhost:5173',
css: {
modules: {
generateScopedName: '[name]__[local]___[hash:base64:5]',
},
},
plugins: [
react(),
federation({
name: "host",
filename: 'remoteEntry-[hash].js',
manifest: true,
remotes: {
microfront_first: {
type: 'var',
name: 'microfront_first',
entry: 'http://localhost:5174/mf-manifest.json',
entryGlobalName: 'microfront_first',
},
microfront_second: {
type: 'var',
name: 'microfront_second',
entry: 'http://localhost:5175/mf-manifest.json',
entryGlobalName: 'microfront_second',
},
},
exposes: {
"./hooks/useGlobalStore": "./src/hooks/useGlobalStore.ts",
"./hooks/useGlobalStoreSelector": "./src/hooks/useGlobalStoreSelector.ts",
"./store/globalStoreProvider": "./src/store/globalStoreProvider.tsx",
},
shared: ["react"],
}),
],
build: {
target: 'chrome89',
},
});
Базовый конфиг модуля:
import { federation } from '@module-federation/vite';
import react from '@vitejs/plugin-react';
import { defineConfig, type PluginOption } from 'vite';
import { dependencies } from './package.json';
function getSingleReactRefreshPlugin(): PluginOption {
return {
name: 'single-react-refresh',
enforce: 'pre',
transform(code, id) {
if (/\.(js|ts|jsx|tsx)$/.test(id)) {
const updatedCode = code.replace(
/import RefreshRuntime from "\/@react-refresh";/g,
'import RefreshRuntime from "http://localhost:5173/@react-refresh";',
);
return updatedCode;
}
return null;
},
};
}
export default defineConfig({
server: {
port: 5174,
origin: 'http://localhost:5174',
// Настроки hmr (оверлей для микрофронтов - отключен из-за нюансов концепции микрофронтов)
hmr: { overlay: false },
},
base: 'http://localhost:5174',
build: {
target: 'esnext',
},
plugins: [
react(),
getSingleReactRefreshPlugin(),
federation({
filename: 'remoteEntry-[hash].js',
manifest: true,
name: 'microfront_first',
exposes: {
'./MicrofrontFirst': './src/App.tsx',
},
remotes: {
host: {
type: 'var',
name: 'host',
entry: 'http://localhost:5173/mf-manifest.json',
entryGlobalName: 'host',
},
},
shared: {
react: {
requiredVersion: dependencies.react,
singleton: true,
},
},
}),
],
})
Без проблем не обошлось
На этапе распила монолита и перехода на микрофронтенды мы столкнулись с практическими сложностями. Некоторые из них были связаны с техническими ограничениями инструментария, другие — с организацией процессов разработки в новой архитектуре. Эти проблемы не всегда очевидны на старте, особенно если проект уже живой, со сложившейся инфраструктурой.
В итоге мы выделили ключевые моменты, которые важно учитывать при переезде:
Как управлять состоянием приложения?
Где хранить общие компоненты, хуки, утилиты?
Как организовать локальную разработку с поддержкой HMR?
Как будет устроен деплой?
Ниже разберём каждый из них подробнее.
Как управлять состоянием приложения?
Хотя идеологически Module Federation предполагает независимость модулей, добиться 100% изоляции сложно, особенно при наличии существующей кодовой базы. Некоторые данные (пользователь, тема, фильтры и т. д.) нужны во всех модулях. Эта информация запрашивается с бэка и помещается в redux-store в host-контейнере.
В этом месте можно придумать множество решений: например избавиться от redux в пользу стейт-менеджеров с концепцией независимых сторов, или прокладывание шины событий между приложениями (event bus). Но для нашего проекта мы выбрали не менять стейт-менеджер (Redux) и воплотить комбинированный подход реализации хранения состояния всего приложения. В host-контейнере создается глобальный стор, который используется в самом host-контейнере и в микрофронтендах только в случае необходимости. И в каждом микрофронте создается собственный стор, который используется и управляется только в рамках модуля.
Какие здесь могут возникнуть проблемы:
Нарушение концепции redux. Она предусматривает наличие одного хранилища в приложении. Но в рамках приложения, где каждый модуль условно независим, эта концепция может быть нарушена. В документации также сказано, что такое решение имеет место быть. Сторы разделяются при помощи пропса “context” в redux provider.
Типизация селекторов и диспатчей. В каждом приложении разворачивается свой redux-store со своими dispatch и selector’s. Чтобы использовать функции из глобального хранилища, мы написали хуки, которые содержат все операции для манипуляций с глобальным хранилищем. Сам host-контейнер, где и находится глобальный стор — это микросервис, который может также выносить наружу функции и компоненты.
Для работы концепции создаются хуки:
-
useGlobalStore — содержит все диспатчи в глобальном хранилище. useGlobalStoreDispatch — хук для диспатча в глобальный стор.
export default function useGlobalStore() { const dispatch = useGlobalStoreDispatch(); const incrementGlobalCounter = () => { dispatch(incrementCounter()); }; const decrementGlobalCounter = () => { dispatch(decrementCounter()); }; const clearGlobalCounter = () => { dispatch(clearCounter()); }; return { incrementGlobalCounter, decrementGlobalCounter, clearGlobalCounter }; }
useGlobalStoreSelector — для селекта данных из глобального хранилища.
import { createSelectorHook } from "react-redux";
import type { TypedUseSelectorHook } from "react-redux";
import { GlobalReduxContext, RootState } from "../store";
export const useGlobalStoreSelector: TypedUseSelectorHook<RootState> = createSelectorHook(GlobalReduxContext);
Также нужен глобальный провайдер globalStoreProvider, чтобы иметь доступ к глобальному хранилищу из модулей.
Теперь о сути проблемы. Микрофронты не имеют представления о типах импортируемых компонентов, функций. Поэтому необходимо глобально декларировать типы для этих хуков в каждом микрофронте. Эта проблема не кажется большой, так как зависимостей от глобального модуля должно быть минимальное количество.
declare module "host/hooks/useGlobalStore" {
function useGlobalStore(): {
incrementGlobalCounter: () => void;
decrementGlobalCounter: () => void;
clearGlobalCounter: () => void;
};
export default useGlobalStore;
}
declare module "host/types/storeState" {
export interface CounterState {
value: number;
}
}
declare module "host/hooks/useGlobalStoreSelector" {
import type { CounterState } from "host/types/storeState";
export type RootState = {
counter: CounterState;
};
export interface TypedUseSelectorHook<TState> {
<TSelected>(selector: (state: TState) => TSelected): TSelected;
<Selected = unknown>(selector: (state: TState) => Selected): Selected;
}
export const useGlobalStoreSelector: TypedUseSelectorHook<RootState>;
}
declare module "host/store/globalStoreProvider" {
import React from "react";
type Props = {
children: React.ReactNode;
};
export default function StoreProvider({ children }: Props): JSX.Element;
}
Далее эти хуки/провайдеры используются в микрофронте так, будто они лежат в этом же модуле.
Где хранить общие компоненты, хуки, функции?
У нас есть собственная дизайн-система и общие компоненты/хуки/функции для работы. Чтобы избежать дублирования, был создан модуль shared, в котором были независимые от хранилища и бэкенда сущности. Модуль подключается через pnpm workspaces, в будущем возможна публикация как npm-пакета. Чтобы избежать дублирования кода в бандлах, модуль объявляется как shared в Vite-конфигурации.
Как организовать локальную разработку, да так, чтобы и HMR работал?
В монорепозитории важным моментом при разработке является HMR. Самая главная часть идеологии Vite — использование нативных модулей в режиме разработки. Это позволяет не билдить при каждом изменении весь бандл, а только файл, в котором произошли изменения. Каждое приложение запускается своим dev-сервером Vite. HMR работает только в пределах одного Vite-Dev-сервера.
Плагин для модульной федерации в vite находится в активной разработке, поэтому решения «из коробки» еще нет. Можно запускать микрофронты в режиме build + watch, но это ломает концепцию локальной разработки: после изменений создается бандл каждого модуля, что, по сути, является собранным проектом. Поэтому, для правильной работы HMR в dev-режиме был написан плагин, который перенаправляет запрос на обновление на host-контейнер.
function getSingleReactRefreshPlugin(): PluginOption {
return {
name: 'single-react-refresh',
enforce: 'pre',
transform(code, id) {
if (/\.(js|ts|jsx|tsx)$/.test(id)) {
const updatedCode = code.replace(
/import RefreshRuntime from "\/@react-refresh";/g,
'import RefreshRuntime from "http://localhost:5173/@react-refresh";',
);
return updatedCode;
}
return null;
},
};
}
Еще одна особенность локальной разработки — обработка ошибок внутри микрофронтов. Dev-сервер Vite работает по WebSocket-соединению: если при разработке возникает ошибка, сервер отправляет её в браузер, и она отображается как overlay поверх страницы. Однако overlay встраивается в index.html, которого у микрофронта (он монтируется в host-приложение), и в этом случае пользователь ничего не увидит.
Чтобы обойти это ограничение, мы добавили обертку ErrorBoundary вокруг микрофронта. Она отлавливает любые runtime-ошибки внутри компонента и самостоятельно отображает UI с описанием ошибки — например, с текстом, стеком вызовов и предложением обновить страницу.
<Route
path={ROUTE_PATH}
element={
<ErrorBoundary fallbackRender={MicrofrontErrorFallback}>
<Suspense fallback={<MicrofrontLoader />}>
<Microfront />
</Suspense>
</ErrorBoundary>
}
/>
Возможно, в будущем с Vite можно будет отображать ошибки без зависимости от index.html, но сейчас альтернативы пока нет.
Как будет устроен деплой?
Фактически, деплой микрофронта не отличается от обычного SPA: сборка выкладывается как статика. Взаимодействие между модулями происходит через mf-manifest.json, указанный в конфигурации.
Выводы
Переход от монолитного фронтенда к архитектуре микрофронтендов на базе Vite и Module Federation оказался для нас оправданным и успешным решением. Несмотря на трудности, мы завершили полный переход и вывели обновленную архитектуру в прод.
Ключевые преимущества, которые мы получили:
Масштабируемость системы без потери управляемости. Новая архитектура позволила нам легко добавлять новые функции и интеграции, не создавая узких мест в кодовой базе и инфраструктуре. Разделение на независимые модули помогло избежать конфликта между командами и упростило поддержку.
Упрощенный запуск новых микрофронтов. Благодаря выделенным границам ответственности и стандартизированным интерфейсам, мы смогли быстро подключать новые микрофронты в проект без глубокого вмешательства в общий код. В том числе благодаря общей библиотеки компонентов, хуков и функций (shared).
Возможность независимой разработки отдельных модулей. Сервисы получили автономию. Это значительно ускорило цикл разработки.
Архитектурная гибкость и адаптивность под потребности продукта. Мы можем выбирать разные технологии и подходы для отдельных микрофронтов, адаптируя их под задачи, а также проще масштабировать проект с ростом команды и функционала.
При этом стоит учитывать и текущие ограничения:
Некоторые сложности при организации локальной разработки. Из-за особенностей работы HMR с микрофронтендами и отсутствия полноценной поддержки module federation в Vite (в монорепозитории), нам пришлось создавать кастомные решения для корректного обновления кода и отображения ошибок, что увеличивает порог вхождения.
Недостаток готовых решений «из коробки», особенно в области HMR и обработки ошибок. На сегодняшний день доступные плагины и инструменты для module federation находятся в активной разработке и требуют доработок и обходных путей, что накладывает дополнительную ответственность на команду разработчиков.
В целом, мы остались довольны результатом. Vite — это минималистичный, но мощный инструмент, в котором есть всё необходимое для продуктивной работы. Он обеспечивает быструю сборку, а его плагинная архитектура делает интеграцию новых, в том числе кастомных решений, простой и гибкой.
Даже несмотря на несовершенства текущего состояния плагина для модульной федерации, большинство проблем решаются быстро, а активное развитие сообщества вокруг Vite делает этот процесс еще быстрее.
Комментарии (8)
jakobz
17.06.2025 10:40А можно в одном микрофронте обновить реакт и задеплоить?
nikolaevdevl Автор
17.06.2025 10:40Если имеется в виду, будут ли дружить микрофронты с различными версиями react - да, будут
ksenkso
17.06.2025 10:40А вы действительно таргетите chrome 89 или это в конфиге просто для примера? Если действительно, то можете рассказать, чем обусловлен выбор?
nikolaevdevl Автор
17.06.2025 10:40На этапе тестирования проекта использовали билд под 'chrome 89', чтобы проверить работу top-level-await, а так же работу со старыми браузерами. Это, можно сказать, артефакт после тестирования. В нашем репозитории мы используем дефолтные настройки билда (esnext)
ec5tasy
17.06.2025 10:40В примере не увидел общих зависимостей в корневом package.json монорепы, в реальном проекте так же оставили у каждого приложения свои зависимости?
nikolaevdevl Автор
17.06.2025 10:40Мы решили определять зависимости для каждого микрофронта отдельно, чтобы микрофронты были изолированы. В любой момент микрофронт можно отселить в другой репозиторий, а также при обновлении версии библиотек проверять работоспособность всех микрофронтов не придется. При таком подходе, если есть необходимость, можно обозначить общие зависимости в shared блок объявления плагина в vite.config.ts. Тогда микрофронты будут использовать общие обьявленные модули в браузере
Vishinskis
Спасибо автору! Очень полезная статья