Асинхронная модель загрузки в Module Federation строится вокруг идеи отложенного получения удалённых модулей через динамические запросы к remote-контейнерам. В контексте Webpack это означает, что каждый remote может быть представлен как отдельный runtime-объект, доступный только в момент выполнения, а не на этапе сборки.
Ключевое отличие от статического связывания заключается в том, что remote-модуль не существует в бандле хоста до момента обращения к нему. Его код загружается как отдельный JavaScript-чанк, после чего выполняется и регистрируется в глобальном контейнере.
Асинхронная федерация опирается на три базовых этапа:
Эти этапы в Webpack Module Federation реализуются через runtime API контейнера:
// упрощённая модель remote-контейнера
const container = window.remoteApp;
await container.init(__webpack_share_scopes__.default);
const factory = await container.get("./Button");
const Module = factory();
Каждый шаг является асинхронным, поскольку включает сетевой запрос или ленивую инициализацию зависимостей.
Современные конфигурации используют promise-based remotes, где сам remote определяется как функция, возвращающая Promise:
const remote = () => import("remoteApp/Button");
На уровне Webpack это трансформируется в загрузку remoteEntry.js и последующий доступ к контейнеру.
Более гибкий вариант через фабрику:
const loadRemote = async (scope, module) => {
await __webpack_init_sharing__("default");
const container = window[scope];
await container.init(__webpack_share_scopes__.default);
const factory = await container.get(module);
return factory();
};
Такой подход позволяет централизовать управление загрузкой и добавлять дополнительные слои логики: кеширование, retry, логирование.
Динамический import() является базовым механизмом
ленивой загрузки, поверх которого строится federated runtime:
const Button = await import("remoteApp/Button");
Webpack заменяет этот вызов на обращение к remote-контейнеру, который сначала загружается как отдельный chunk:
import(/* webpackIgnore: true */ "https://cdn.example.com/remoteEntry.js")
После загрузки скрипта происходит регистрация глобального контейнера:
window.remoteApp = {
get: async (module) => { /* ... */ },
init: async (shareScope) => { /* ... */ }
};
Асинхронная загрузка всегда проходит через фиксированный жизненный цикл:
Загрузка remoteEntry.js через script tag или dynamic import.
Инициализация share scope, синхронизация зависимостей между хостом и remote:
await container.init(__webpack_share_scopes__.default);
Получение фабрики модуля:
const factory = await container.get("./module");
const Module = factory();
Важно, что factory() выполняется уже локально и
синхронно, после завершения асинхронных шагов.
Асинхронная природа federated-модулей требует управления состояниями загрузки. Типичная модель включает:
Пример обёртки:
const registry = new Map();
export const loadFederatedModule = async (key, loader) => {
if (registry.has(key)) {
return registry.get(key);
}
const promise = loader()
.then((mod) => {
registry.set(key, mod);
return mod;
});
registry.set(key, promise);
return promise;
};
Такая модель предотвращает повторные запросы и обеспечивает дедупликацию загрузки.
Асинхронная загрузка remote-контейнеров подвержена сетевым сбоям, недоступности CDN и конфликтам версий.
Распространённый подход — retry-стратегия:
const loadWithRetry = async (fn, retries = 3) => {
let lastError;
for (let i = 0; i < retries; i++) {
try {
return await fn();
} catch (e) {
lastError = e;
}
}
throw lastError;
};
Дополнительно применяются fallback-remotes:
const remotes = [
"https://cdn1.example.com/remoteEntry.js",
"https://cdn2.example.com/remoteEntry.js"
];
Загрузка происходит по цепочке до первого успешного результата.
Асинхронная федерация выигрывает от стратегий предзагрузки:
Webpack поддерживает hints:
import(
/* webpackPrefetch: true */
"remoteApp/LazyComponent"
);
В runtime это превращается в добавление
<link rel="prefetch">.
Также применяется ручная предзагрузка:
const prefetchRemote = (url) => {
const link = document.createElement("link");
link.rel = "preload";
link.as = "script";
link.href = url;
document.head.appendChild(link);
};
После загрузки remote-контейнер сохраняется в глобальной области:
window.remoteApp
Однако на уровне приложения требуется более строгий контроль:
const containerCache = new Map();
export const getContainer = async (scope, url) => {
if (containerCache.has(scope)) {
return containerCache.get(scope);
}
await import(/* webpackIgnore: true */ url);
const container = window[scope];
containerCache.set(scope, container);
return container;
};
Такой подход предотвращает повторную загрузку remoteEntry.js.
Shared scope инициализируется один раз на приложение, но его синхронизация происходит асинхронно:
await __webpack_init_sharing__("default");
await container.init(__webpack_share_scopes__.default);
Проблема возникает при конкурирующих загрузках нескольких remotes. В этом случае важно гарантировать единичную инициализацию:
let shareInitPromise;
export const initShare = () => {
if (!shareInitPromise) {
shareInitPromise = __webpack_init_sharing__("default");
}
return shareInitPromise;
};
export const createRemoteLoader = (scope, url) => {
return async (module) => {
await import(/* webpackIgnore: true */ url);
const container = window[scope];
await container.init(__webpack_share_scopes__.default);
const factory = await container.get(module);
return factory();
};
};
const registry = new Map();
export const defineRemote = (name, loader) => {
registry.set(name, loader);
};
export const useRemote = (name) => {
return registry.get(name)?.();
};
const createLazy = (loader) => {
let instance;
return new Proxy({}, {
get: async (_, prop) => {
if (!instance) {
instance = await loader();
}
return instance[prop];
}
});
};
Асинхронная загрузка требует минимизации задержек:
Также важна стратегия cold start:
const warmup = async () => {
await Promise.all([
loadRemote("app1", "/remoteEntry.js"),
loadRemote("app2", "/remoteEntry.js")
]);
};
При несовпадении версий пакетов возможна деградация runtime или fallback на локальные копии.
Одновременная инициализация контейнера приводит к неопределённому поведению без защиты через singleton-паттерны.
Необходимы fallback-цепочки и деградация функциональности.
Возникает при отсутствии централизованного cache layer.
При неправильном порядке init возможны ошибки резолвинга зависимостей внутри remote.