Module Federation and micro-frontends

webpack · Expert · 15 min

Large organisations use it to compose apps from separately deployed micro-frontends.

Module Federation (webpack 5's ModuleFederationPlugin) lets one build load modules from another build at runtime. Each build can be:

  • A remote: it exposes modules ("./Button": "./src/Button.jsx") and emits a small remoteEntry.js manifest.
  • A host: it declares remotes (checkout: "checkout@https://cdn.example.com/checkout/remoteEntry.js") and imports them like packages: import("checkout/Cart").

Shared dependencies are negotiated at runtime: with shared: { react: { singleton: true, requiredVersion: "^19.0.0" } }, host and remotes use one copy of React instead of each bundling their own. Version mismatches produce warnings or fall back to separate copies, depending on settings.

Practical rules:

  • The host must load shared modules asynchronously, so use a bootstrap pattern: index.js contains only import("./bootstrap"), giving the runtime time to negotiate shared versions. Otherwise you hit "Shared module is not available for eager consumption".
  • Treat exposed modules as public APIs with versioning and contracts; independent deploys mean independent failures. Wrap remote loads in error boundaries.
  • Type sharing, dynamic remotes and runtime plugins come from Module Federation 2.0 (@module-federation/enhanced), which also works with Rspack, Vite and other bundlers.

Federation solves organisational problems (independent deploys of a large frontend) at the price of runtime complexity; a monorepo with shared packages is often simpler.

Example

// shell app (host): webpack.config.js
const { ModuleFederationPlugin } = require("webpack").container;

module.exports = {
  entry: "./src/index.js",                 // contains only: import("./bootstrap");
  output: { publicPath: "auto" },
  plugins: [
    new ModuleFederationPlugin({
      name: "shell",
      remotes: {
        checkout: "checkout@https://cdn.example.com/checkout/remoteEntry.js",
      },
      shared: {
        react: { singleton: true, requiredVersion: "^19.0.0" },
        "react-dom": { singleton: true, requiredVersion: "^19.0.0" },
      },
    }),
  ],
};
// checkout app (remote): new ModuleFederationPlugin({ name: "checkout",
//   filename: "remoteEntry.js", exposes: { "./Cart": "./src/Cart.jsx" }, shared: {...} })
// in the shell: const Cart = React.lazy(() => import("checkout/Cart"));

Remember

  • Remotes expose modules via remoteEntry.js; hosts import them at runtime
  • Shared singletons keep one copy of React across builds
  • Use an async bootstrap entry for shared modules
  • Federation trades runtime complexity for independent deploys

Watch out for

  • Importing shared modules eagerly in the host entry, causing 'not available for eager consumption'.

Check yourself

  1. What does `singleton: true` on a shared dependency ensure?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: webpack.js.org/concepts/module-federation/