Module Federation and Micro-Frontends in JavaScript
AI generated
JS
() =>
JavaScript · Module Federation · Micro-Frontends · Webpack 5
Module Federation and Micro-Frontends
Host, Remote, Shared Dependencies and Deployment

Module Federation in Webpack 5 solves what has long been the hardest problem in micro-frontend architectures: how multiple independent teams can combine their JavaScript bundles at runtime, without loading shared dependencies multiple times or sharing a central build pipeline.

18 min read Webpack 5 · Vite Federation · Host · Remote · Shared · Dynamic Import Node.js · React · Vue · Vanilla JS

1. The problem Module Federation solves

Large frontend applications tend to grow into hard-to-maintain monoliths over time: a single repository, a single build pipeline, a single deployment. When 15 teams work on the same codebase, every merge becomes a coordination problem and every deployment becomes a risk for everyone. Micro-frontends are the answer: the same frontend application is split into independent, team-owned parts that can be developed, tested and deployed separately. That sounds simple, but the technical sticking point for a long time was how these parts come together in the browser without React being loaded three times or global CSS classes overwriting each other.

Module Federation, introduced with Webpack 5, is the first build-tool-native solution to this problem. It allows a JavaScript application to load code at runtime from another, completely separate Webpack build, with explicit control over shared dependencies and without the overhead of a central build step. This is not a new idea: iframes, script tags and Single-SPA have all pursued the same goal. But Module Federation is the first solution that is deeply integrated into the JavaScript module system and therefore works naturally with modern frameworks.

2. Host, Remote and Shared: the three core concepts

The Module Federation model is built on three roles. A host is the application that loads other modules, typically the shell or the app skeleton. A remote is an application that provides modules for other hosts, a team-owned area of the application that is deployed separately. The same Webpack bundle can be both host and remote at the same time: a product detail app can load modules from a reviews app (consuming a remote) while simultaneously providing its own components to a parent shell (acting as a remote).

Shared is the third concept: libraries that should be shared between host and remote without being loaded into the browser multiple times. React is the classic example: if both host and remote load React, both React instances would be active, which leads to bugs because React internally relies on global state (hooks, context). With shared: { react: { singleton: true } }, Module Federation ensures that only one React instance exists in the browser, no matter which part of the application loads first.


// webpack.config.js: HOST application (App Shell)
const { ModuleFederationPlugin } = require("webpack").container;

module.exports = {
  mode: "development",
  plugins: [
    new ModuleFederationPlugin({
      name: "shell",           // unique name for this application
      remotes: {
        // Reference remote applications by name + their manifest URL
        productApp: "productApp@https://products.mironsoft.de/remoteEntry.js",
        cartApp:    "cartApp@https://cart.mironsoft.de/remoteEntry.js",
      },
      shared: {
        react:     { singleton: true, requiredVersion: "^18.2.0" },
        "react-dom": { singleton: true, requiredVersion: "^18.2.0" },
      },
    }),
  ],
};

// webpack.config.js: REMOTE application (Product Team)
module.exports = {
  mode: "development",
  plugins: [
    new ModuleFederationPlugin({
      name: "productApp",       // must match the key in host remotes
      filename: "remoteEntry.js", // manifest file, must be publicly accessible
      exposes: {
        // Map public names to local module paths
        "./ProductCard":   "./src/components/ProductCard",
        "./ProductDetail": "./src/pages/ProductDetail",
        "./useCart":       "./src/hooks/useCart",
      },
      shared: {
        react:     { singleton: true, requiredVersion: "^18.2.0" },
        "react-dom": { singleton: true, requiredVersion: "^18.2.0" },
      },
    }),
  ],
};

4. Shared dependencies: versioning and singleton

The shared dependencies concept in Module Federation is the most complex part of the configuration and the most common source of errors. When host and remote declare different versions of a library, Module Federation uses semver compatibility to decide whether a single version is sufficient for both, or whether both versions need to be loaded. With requiredVersion: "^18.2.0", a bundle signals that it is compatible with any patch or minor version 18.2.0 or above. If the highest compatible version is already loaded, it gets reused, no second download.

The singleton: true flag forces at most one instance to exist in the browser, even if host and remote had incompatible versions. In that case a warning is emitted and the higher version wins. For React, singleton is mandatory, because two React instances in the browser lead to the well-known "Invalid hook call" error. For other libraries like Lodash or Axios, singleton is optional: multiple instances can coexist, the application still works, it just wastes bandwidth. The eager: true option loads the library immediately instead of only on first import, which matters for the app entry point, which would otherwise have to wait for the asynchronous loading process.

5. Dynamically loading remote modules at runtime

The static configuration in webpack.config.js is only half the story. Module Federation also supports fully dynamic loading of remote modules at runtime, without the remote bundle's URL needing to be known at build time. This enables scenarios such as A/B tests, where the server decides at runtime which version of a component to load, or plugin systems, where end users can integrate their own modules.

The dynamic remote loading pattern uses the __webpack_init_sharing__ and __webpack_share_scopes__ APIs to manually initialize the shared scope before a dynamically loaded remote module is used. At its core, this comes down to an import() of the remote manifest, followed by a call to the remote container's init and get functions. This mechanism makes Module Federation the foundation for genuine plugin architectures on the frontend, where third parties can provide modules that the host application integrates at runtime.


// Dynamic remote loading, URL not known at build time
async function loadRemoteModule(remoteUrl, scope, module) {
  // Step 1: inject the remote entry script dynamically
  await new Promise((resolve, reject) => {
    const script = document.createElement("script");
    script.src = remoteUrl;
    script.onload = resolve;
    script.onerror = reject;
    document.head.appendChild(script);
  });

  // Step 2: initialize the shared scope
  await __webpack_init_sharing__("default");

  // Step 3: get the container from the window (set by remoteEntry.js)
  const container = window[scope];
  await container.init(__webpack_share_scopes__.default);

  // Step 4: get the requested module factory
  const factory = await container.get(module);
  return factory(); // returns the module
}

// Usage: load any remote at runtime
async function renderProductCard(productId) {
  const { default: ProductCard } = await loadRemoteModule(
    "https://products.mironsoft.de/remoteEntry.js",
    "productApp",   // window[scope] must match the remote's `name`
    "./ProductCard" // must be listed in the remote's `exposes`
  );
  // Use ProductCard as a normal React component
}

// React lazy + Suspense integration
const RemoteProductCard = React.lazy(() =>
  import("productApp/ProductCard") // static reference via webpack remotes config
);
// Usage: <Suspense fallback={<Spinner />}><RemoteProductCard id={1} /></Suspense>

6. Module Federation with Vite

While Module Federation is natively integrated into Webpack 5, the ecosystem offers @originjs/vite-plugin-federation and the more official @module-federation/vite (since 2024) as plugins for Vite-based projects. The configuration follows the same host/remote/shared pattern as in Webpack, but is wired in as a Vite plugin inside vite.config.ts. One important difference: Vite uses ES modules and native browser imports in development mode, while the production build is compiled into a Webpack-compatible format.

Interoperability between Webpack hosts and Vite remotes (and vice versa) is technically possible, but requires careful alignment of the remoteEntry.js formats. The newer "Module Federation 2.0" specification aims for build-tool agnosticism and is meant to enable seamless interoperability between Webpack, Vite, Rspack and others. For new projects, it is advisable to standardize the entire micro-frontend ecosystem on a single build tool and use Vite 5 with @module-federation/vite or Rspack with native federation support.

7. Routing in micro-frontend architectures

Routing is one of the trickiest aspects of micro-frontend architectures with Module Federation. If every remote module brings its own router setup, conflicts arise: which router is responsible for which URL? The most common pattern is "top-level routing in the host, sub-routing in the remote": the host router decides, based on the URL path, which remote module to load. The remote module receives a base URL as a prop and renders its own routing beneath that base path.

For React projects, this specifically means: the host uses React Router v6 with a <Route path="/products/*"> catch-all that loads the remote module. The remote receives basename="/products" and renders its own routes relative to that. A common bug: the remote module imports BrowserRouter instead of MemoryRouter or a Router using the history object passed down from the host. This results in two competing router instances, both writing to the browser history stack and overwriting each other's URL.

8. Deployment strategies and CDN configuration

The biggest promise of Module Federation is independent deployment: every team can deploy its remote module without informing other teams. But this promise only holds if the deployment is configured correctly. The publicPath in the Webpack configuration must match the actual URL under which the bundle is served. For CDN deployments, publicPath: "auto" should be used, so that Webpack automatically uses the URL of remoteEntry.js as the base for all other chunk URLs.

Versioning the remote entries is crucial for zero-downtime deployments. If a host loads a new version of the remote while the old bundle is still in the browser cache, incompatibilities can occur. The standard pattern: remoteEntry.js is never cached (Cache-Control: no-store or a short TTL), while all other chunks are cached long-term using content-based hashing. The host always loads the current remoteEntry.js, which then points to the correct chunk URLs. This mechanism makes Module Federation deployments as reliable as traditional CDN deployments.

9. Module Federation vs. other approaches

There are several technical approaches to micro-frontend architectures that differ in isolation, performance and complexity.

Approach Isolation Shared Dependencies Complexity
Module Federation JS scope (no CSS) Automatic via plugin Medium
iframes Full isolation None, everything loaded multiple times Low
Web Components Shadow DOM for CSS Manual via import maps Medium
Single-SPA JS scope Manual via SystemJS High
Import Maps None Native via browser Low

// vite.config.ts: Vite Module Federation (host)
import { defineConfig } from "vite";
import federation from "@originjs/vite-plugin-federation";

export default defineConfig({
  plugins: [
    federation({
      name: "shell",
      remotes: {
        productApp: "https://products.mironsoft.de/assets/remoteEntry.js",
      },
      shared: ["react", "react-dom"],
    }),
  ],
  build: {
    target: "esnext", // required for top-level await in federation runtime
    minify: false,    // easier debugging during federation setup
  },
});

// vite.config.ts: Vite Module Federation (remote)
export default defineConfig({
  plugins: [
    federation({
      name: "productApp",
      filename: "remoteEntry.js",
      exposes: {
        "./ProductCard": "./src/components/ProductCard.tsx",
      },
      shared: ["react", "react-dom"],
    }),
  ],
  build: {
    target: "esnext",
  },
});

// Usage in host component, same as Webpack federation
// const ProductCard = React.lazy(() => import("productApp/ProductCard"));

Mironsoft

Micro-frontend architecture, Module Federation and frontend infrastructure

Want to introduce micro-frontend architecture for your team?

We design and implement Module Federation architectures for multi-team frontends, from Webpack/Vite configuration through shared dependency strategies to the CDN deployment pipeline.

Architecture design

Host/remote split, shared dependency strategy and routing concept

Build configuration

Webpack 5 and Vite Federation plugin setup with CI/CD integration

CDN deployment

publicPath, caching strategy and zero-downtime deployment for remote entries

10. Summary

Module Federation is the foundation of modern micro-frontend architectures: it enables loading JavaScript modules from other, separately deployed applications at runtime. The host/remote/shared model gives teams full autonomy over development and deployment, while the shared dependency mechanism ensures that React, Vue and other frameworks are not loaded into the browser multiple times. Webpack 5 has integrated federation natively; Vite plugins close the gap for Vite-based projects.

The most important pitfalls: singleton configuration for React and context-dependent libraries, correct publicPath handling for CDN deployments, and a clear routing strategy that prevents multiple router instances from writing to the browser history stack at the same time. Module Federation is not a technology for every project, but from around three independent teams with separate deployment cycles onward, the investment in the infrastructure starts to pay off through reduced coordination costs.

Module Federation and Micro-Frontends: The Essentials at a Glance

Host & Remote

Host consumes remote modules at runtime. Remote exposes modules via remoteEntry.js. Both can be host and remote at the same time.

Shared Dependencies

singleton: true for React and context-dependent libs. requiredVersion for semver compatibility checks. eager: true for the app entry point.

Deployment

publicPath: "auto" for CDN. remoteEntry.js without cache (no-store). Cache chunks with content hash long-term. Teams deploy independently.

Vite & Alternatives

@module-federation/vite for Vite projects. Module Federation 2.0 for build-tool agnosticism. Rspack with native federation support as a Webpack alternative.

11. FAQ: Module Federation and Micro-Frontends

1What is Module Federation?
A Webpack 5 feature for loading modules from other builds at runtime. Technical foundation for micro-frontend architectures with shared dependencies.
2Host vs. remote?
Host loads modules from remotes. Remote provides modules via remoteEntry.js. The same app can be both host and remote at the same time.
3Why singleton: true for React?
Two React instances lead to "Invalid hook call". singleton: true enforces a single instance in the browser.
4Deployment with Module Federation?
remoteEntry.js: no cache. Chunks: content hash plus long cache. publicPath: auto. Teams deploy independently without blocking each other.
5Vite and Module Federation?
@module-federation/vite plugin. Configuration like Webpack. Build target: esnext. Interop between Webpack and Vite is possible but complex.
6Resolving routing conflicts?
Top-level routing in the host, sub-routing in the remote with basename. Never import BrowserRouter in the remote, otherwise you get two competing routers.
7Dynamic remote loading?
Inject a script dynamically, call __webpack_init_sharing__, then window[scope].init plus container.get(). Enables plugin systems and A/B tests.
8From how many teams is it worth it?
From around 3 teams with separate deployment cycles onward. Below that, the infrastructure complexity outweighs the benefit.
9Module Federation 2.0?
A build-tool-agnostic specification for Webpack, Vite and Rspack interop. Standardized remoteEntry format as the goal.
10Difference to iframes?
iframes: full isolation, but difficult UX integration. Module Federation: shared JS scope, natural framework integration, no CSS isolation without Shadow DOM.