Webpack Module Federation в NextJS
Мне тут понадобилось подключить в приложение NextJS и я столкнулся с большими проблемами. В этой статье я постараюсь коротко и емко на примере показать как подключить Webpack Module Federation (Далее MF) компоненты на ваше приложение NextJS (на клиентской стороне).

Проблема
До недавнего времени, официальным решением внедрения MF модулей в NextJS была библиотека @module-federation/nextjs-mf.
Плагин @module-federation/nextjs-mf — это специальный модуль, который упрощает интеграцию Webpack Module Federation в приложения на Next.js. Он создан командой, которая разрабатывает сам Module Federation, и решает множество проблем, связанных с SSR, shared-зависимостями и конфигурацией Next.js.
Next.js по умолчанию не поддерживает Module Federation “из коробки”, особенно в части SSR (Server Side Rendering). Этот плагин:
- Упрощает настройку Webpack Module Federation в Next.js
- Добавляет поддержку SSR и динамической загрузки remote компонентов
- Обеспечивает корректную работу shared зависимостей (например, React)
- Позволяет использовать dynamic() из next/dynamic с federation-компонентами
- Работает как на клиенте, так и на сервере
- Позволяет делать federated middleware (например, отданные с getServerSideProps)
- Позволяет делать hot-reload remote модулей в dev-среде
Но вот незадача, она deprecated.

Команда разрабатывающая решение MF более не развивает плагин и вскоре прекратит поддержку.
Стоит отметить что в последнем сообщении Zack Jackson (ментейнер MF) отмечает:
- Поддержка Module Federation в Next.js — вероятна (~80%), но не гарантирована.
- Вектор развития положительный, технические и организационные барьеры уменьшаются.
- Плагин nextjs-mf получит частичное восстановление и поддержку App Router в ближайшем будущем.
Надежда умирает последней...
Решение
Но не стоит отчаиваться. NextJS собирается Webpack, а значит под капотом все те модули, а значит можно просто использовать плагин ModuleFederationPlugin из самого webpack.
Далее в статье мы создадим Remote приложение (без NextJS) для раздачи виджетов, и Host на основе NextJS и подключим к нему модули WMF из Remote
Я подготовил два репозитория:
1) Host
(приложение NextJS) в который будем подключать приложение (https://github.com/Hydrock/wmf-nextjs)
2) Remote
- отсюда будем отдавать компонент (https://github.com/Hydrock/wmf-remote)
Скачайте оба репозитория в удобную для вас директорию и запустите (Смотрите инструкцию).
В NextJS приложении на http://localhost:3000
должен подружаться удаленный компонент из Remote приложения.
Теперь посмотрим как это сделано.
Разбор
В приложении wmf-nextjs
откройте конфиг next.config.ts
1const { container } = require('webpack');
2const { ModuleFederationPlugin } = container;
3import type { NextConfig } from "next";
4
5const nextConfig: NextConfig = {
6 webpack(config, { isServer }) {
7 if (!isServer) {
8 console.log('✅ Webpack client config is used');
9
10 config.plugins.push(
11 new ModuleFederationPlugin({})
12 );
13 }
14
15 return config;
16 },
17};
18
19module.exports = nextConfig;
20
Тут мы просто подключили плагин ModuleFederationPlugin из Webpack. Не указывали remote и exposes параметры - все это будет делаться динамически в компоненте. Мы же хотим в будущем загружать модули из разных источников.
Файл components/RemoteWrapper.tsx
- обертка над загружаемым компонентом. Обратите внимание на 'use client'
- мы рендерим/загружаем модуль только на клиенте/в браузере.
Файл RemoteWidget.tsx
- сам загрузчик удаленного модуля. В идеале, конечно, все это нужно оформить в отдельную библиотеку загрузчик с обработкой ошибок, но я специально этого не делал для простоты примера.
1// components/RemoteWidget.tsx
2'use client';
3
4import React, { useEffect, useState } from 'react';
5
6function injectScript(url: string, scope: string): Promise<void> {
7 return new Promise((resolve, reject) => {
8 // eslint-disable-next-line
9 // @ts-ignore
10 if (window[scope]) return resolve(); // уже есть
11
12 const existingScript = document.querySelector(`script[src="${url}"]`);
13 if (existingScript) return resolve(); // уже загружен
14
15 const script = document.createElement('script');
16 script.src = url;
17 script.type = 'text/javascript';
18 script.async = true;
19
20 script.onload = () => {
21 const checkInterval = setInterval(() => {
22 // eslint-disable-next-line
23 // @ts-ignore
24 if (window[scope]) {
25 clearInterval(checkInterval);
26 resolve();
27 }
28 }, 20);
29
30 // если через 3 сек не появился контейнер — ошибка
31 setTimeout(() => {
32 clearInterval(checkInterval);
33 reject(new Error(`🛑 ${scope} не появился в window после загрузки`));
34 }, 3000);
35 };
36
37 script.onerror = () => reject(new Error(`❌ Ошибка загрузки ${url}`));
38 document.head.appendChild(script);
39 });
40}
41
42const RemoteWidget = () => {
43 const [Comp, setComp] = useState<React.ComponentType | null>(null);
44
45 // INFO: при первом вызове useEffect container равен undefined
46 // но при втором он успевает инициализироваться
47 // но вот что заставляет запустить useEffect второй раз - не знаю
48 useEffect(() => {
49 const load = async () => {
50 const remoteUrl = 'http://localhost:8082/remoteEntry.js';
51 const scope = 'remoteApp';
52 const module = './RemoteComponent';
53
54 await injectScript(remoteUrl, scope);
55
56 // @ts-ignore — Webpack runtime
57 await __webpack_init_sharing__('default');
58
59 // @ts-ignore
60 const container = window[scope];
61 if (!container) throw new Error(`Remote container ${scope} не найден в window`);
62
63 // 🔧 если нет shared, передай пустой объект
64 // eslint-disable-next-line
65 // @ts-ignore
66 await container.init(typeof __webpack_share_scopes__ !== 'undefined'
67 // eslint-disable-next-line
68 // @ts-ignore
69 ? __webpack_share_scopes__.default
70 : {}
71 );
72
73 const factory = await container.get(module);
74 const Module = factory();
75
76 setComp(() => Module.default || Module);
77 };
78
79 load().catch(console.error);
80 }, []);
81
82 if (!Comp) return <div>Загрузка виджета...</div>;
83 return <Comp />;
84};
85
86export default RemoteWidget;
В этом загрузчике есть баг, почему то вставка скрипта remoteEntry.js из удаленного модуля, и последующая инициализация scope происходит позже, чем webpack попытается его использовать - поэтому в консоль выподает ошибка. Но, при повторном рендере компонента все работает хорошо. Я надеюсь это починить в коде и исправить статью, до того как вы это прочитаете. В любом случае, вариант рабочий.
В коде есть функция injectScript. Его задача, добавить тег script на страницу с remoteEntry.js файлом из удаленного приложения. Когда этот файл загружается, он "сообщает" webpack-у какие удаленные модули есть в наличии и как их загрузить.
Когда скрипт загружен, срабатывает событие script.onload - запускается интервал и в нем проверяется, что scope инициализирован на объекте window
(window[scope]
). Все таки скрипту, нужно некоторое время для инициализации. После этого созданный выше промис резолвится.
В компоненте RemoteWidget мы ждем когда scope будет точно инициализирован - await injectScript(remoteUrl, scope);
Далее четь разберемся подробнее.
Функция __webpack_init_sharing__
— это внутренняя часть Webpack Module Federation Runtime, и она играет ключевую роль в том, как работает механизм shared (разделяемых) зависимостей между host и remote.
Когда вы вызываете await __webpack_init_sharing__('default');
вы инициализируете share scope с именем 'default', где Webpack:
- создаёт или подключается к глобальному контейнеру зависимостей (обычно __webpack_share_scopes__
)
- определяет, какие модули доступны как shared
- обеспечивает, чтобы singleton модули (например, react) были реально одинаковыми
- запускает механизм сравнения версий, если указаны requiredVersion и strictVersion
Технически…
В примере мы шарим (remote приложение):
1shared: {
2 react: {
3 eager: true,
4 singleton: true,
5 },
6}
__webpack_init_sharing__('default')
создаёт глобальный скоуп (если его ещё нет)
туда помещается react
как singleton
при загрузке remote
, его container.init()
синхронизируется с этим скоупом
Это ключевой механизм, который позволяет host и remote использовать одну и ту же копию React, и избежать ошибок типа:Invalid hook calluseContext(null)Cannot read properties of undefined (reading 'useLayoutEffect')
Далее, в нашем коде:
1const container = window[scope];
2
3await container.init(typeof __webpack_share_scopes__ !== 'undefined'
4 ? __webpack_share_scopes__.default
5 : {}
6);
получаем remote-контейнер, зарегистрированный Webpack-ом как self[scope] (например, remoteApp, как у нас в примере)
он должен содержать методы init()
и get()
вызываем init
, чтобы контейнер подключился к глобальному набору shared-зависимостей (это обязательно, если ты используешь shared)
если ты не вызовешь init()
, и в remote
объявлены shared
, произойдёт ошибка
container.init(...)
обязан быть вызван перед get(), иначе remote не сможет синхронизировать зависимости.
1const factory = await container.get(module);
2const Module = factory();
container.get('./RemoteComponent')
- это асинхронный вызов, который:
находит нужный модуль внутри remoteEntry.js
загружает его chunk
(если нужно)
возвращает фабрику (factory), то есть функцию, которая создаёт модуль
Важно: модуль не возвращается напрямую, а через factory() — это особенность Webpack runtime
factory()
возвращает экспортированное содержимое модуля
это либо объект Module
, содержащий default
экспорт, либо просто сам компонент
Ну а далее у нас установка модуля/компонента в стейт и последующее его использование.
1setComp(() => Module.default || Module);
Вообщем все сложно... Но мне хотелось немного разъяснить, что тут происходит.
Заключение
Хоть официальная библиотека поддержки MF в NextJS теперь deprecated - использование MF в NextJS все же возможно. Да, только на клиентской стороне, да, SSR придется реализовывать/придумывать самому, но - это реально.
Если вы заходите улучшить пример - создайте Issue в одном из репозиториев или можете сразу делать PR.
Спасибо за внимание!