Webpack 5 Module Federation: Practical Decisions for Sharing Code Across Builds
Webpack 5 Module Federation lets independent builds share code at runtime. This post covers the practical decisions — shared dependency negotiation, singleton React, lazy vs eager loading, CORS, and failure handling — with a concrete host/remote example and a verification checklist.
02 May 2026, 21:34 UTC

The problem: independent deployments that still share code
Teams building micro-frontends often hit a wall: each frontend should deploy independently, yet they all need the same React version, design system, or utility library. Copying dependencies into every build bloats bundles and creates version drift. NPM packages solve sharing but add publish/deploy latency and version-locking overhead. Webpack 5's Module Federation addresses this by letting separate Webpack builds exchange modules at runtime — no registry, no extra tooling.
How the runtime exchange works
A remote build declares which modules it exposes (e.g., Button, ThemeProvider) and which dependencies it's willing to share (React, ui-lib). It emits a tiny manifest called remoteEntry.js mapping exposed names to chunk URLs. A host build references that manifest and uses dynamic import('remote/Button'). At runtime, Webpack fetches the manifest, downloads the needed chunk, evaluates it, and returns the module namespace — all without a build-time dependency between the two projects.
Shared dependency negotiation in practice
The shared configuration is where most engineering decisions live. Each participant lists packages with version ranges and behavior flags:
// webpack.config.js (remote)
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'designSystem',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/Button',
'./Theme': './src/ThemeContext'
},
shared: {
react: { singleton: true, eager: true, requiredVersion: '^18.2.0' },
'ui-lib': { singleton: false, eager: false, requiredVersion: '^2.0.0' }
}
})
]
};
singleton: true forces a single instance across all builds — critical for React to avoid duplicate contexts and hooks violations. eager: true includes the module in the initial chunk (use only when the host always needs it at startup). requiredVersion expresses a semver range; Webpack picks the highest compatible version across all participants. If negotiation fails, each build falls back to its own copy.
Worked example: shell app consuming a component library
Two minimal Webpack 5 projects, each running its own dev server:
- Remote (port 3001) — exposes
./DesignSystemand shares React as a singleton. - Host (port 3000) — declares the remote via
remotes: { designSystem: 'designSystem@http://localhost:3001/remoteEntry.js' }and lazy-loads the design system on route change.
// Host route component
const DesignSystem = React.lazy(() => import('designSystem/DesignSystem'));
function App() {
return (
<Suspense fallback={<Spinner />}><DesignSystem /></Suspense>
);
}
When the user navigates, the host fetches http://localhost:3001/remoteEntry.js, reads the chunk URL for ./DesignSystem, downloads and evaluates it. React comes from the host's bundle (singleton), so both builds share the same instance.
Trade-offs and failure modes
- Runtime coupling: If the remote server is down or serves a broken
remoteEntry.js, the host's dynamic import rejects. You need error boundaries and a fallback UI — Module Federation doesn't provide one. - Cache invalidation: Updating a remote requires changing its chunk filenames (Webpack's content hashes handle this) and ensuring the host fetches the new
remoteEntry.js. Version the manifest URL (remoteEntry.[contenthash].js) or use short cache TTLs. - CORS: The remote must serve
Access-Control-Allow-Originheaders allowing the host's origin, otherwise the manifest fetch fails silently in the network tab. - TypeScript: Without
@module-federation/typescriptor manualdeclare module 'designSystem/*'declarations, the host seesanytypes for remote imports.
Verification checklist before shipping
- Run both dev servers; open host in browser; verify the lazy-loaded remote component renders.
- Inspect the Network tab: confirm
remoteEntry.jsreturns a JSON manifest with chunk mappings. - Stop the remote server; reload host — observe the chunk load error and test your error boundary.
- Run
webpack --jsonon each build; checkmodulesfor shared dependency resolution (look forwebpack.container.sharedentries).
Closing: treat the contract as infrastructure
Module Federation moves the integration point from package.json to runtime HTTP. That's powerful — but it means your deployment pipeline must guarantee remote availability, versioned manifest URLs, and CORS configuration. Start with a single shared singleton (React) and one lazy-loaded remote. Add eager shares only when profiling shows a real startup cost. And always ship an error boundary: the network will fail.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.