Sharing Code at Runtime with Webpack 5 Module Federation
Learn how Module Federation lets independent builds exchange modules on demand, reducing bundle size and enabling micro‑frontend workflows.
02 Dec 2025, 16:30 UTC

The problem: duplicated bundles and tight coupling
Teams that split a product into several repositories often end up shipping the same utility library or UI component in every build. The result is larger initial downloads and a coordination burden whenever a shared dependency changes. Webpack 5’s Module Federation addresses this by letting a running application pull modules from another build on demand, so each deployable stays small and can be updated independently.
Core concepts
- Name – a unique identifier for the container (e.g.,
hostApporremoteApp). - Remotes – a map from a logical name to the URL of the remote’s entry script (
remoteEntry.js). - Exposes – in the remote, the list of modules that other containers may request.
- Shared – a declaration of packages that should be deduplicated across containers (React, lodash, etc.). Webpack will negotiate a single version at runtime.
All of these live in webpack.config.js under the ModuleFederationPlugin options.
Minimal working example
1. Scaffold two projects
mkdir host-app remote-app
cd remote-app && npm init -y
cd ../host-app && npm init -y
Install the same Webpack 5 toolchain in both folders (requires Node ≥ 14, npm ≥ 7):
npm i -D webpack@5 webpack-cli@5 webpack-dev-server@4
2. Remote configuration (remote-app/webpack.config.js)
const { ModuleFederationPlugin } = require('webpack').container;
const path = require('path');
module.exports = {
entry: './src/index.js',
mode: 'development',
devServer: { port: 8081, headers: { 'Access-Control-Allow-Origin': '*' } },
output: { publicPath: 'auto' },
plugins: [
new ModuleFederationPlugin({
name: 'remoteApp',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/Button.jsx',
},
shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
}),
],
};
Create src/Button.jsx exporting a simple React component.
3. Host configuration (host-app/webpack.config.js)
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
entry: './src/index.js',
mode: 'development',
devServer: { port: 8080 },
output: { publicPath: 'auto' },
plugins: [
new ModuleFederationPlugin({
name: 'hostApp',
remotes: {
remoteApp: 'remoteApp@http://localhost:8081/remoteEntry.js',
},
shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
}),
],
};
In host-app/src/index.js load the remote component lazily:
async function loadButton() {
const { default: Button } = await import('remoteApp/Button');
const root = document.getElementById('root');
root.appendChild(React.createElement(Button));
}
loadButton();
4. Run both dev servers
# Terminal 1 – remote
cd remote-app && npx webpack serve
# Terminal 2 – host
cd host-app && npx webpack serve
Open http://localhost:8080. The browser console should show a request for remoteEntry.js only when loadButton() executes, and the Button component renders.
What happens at runtime
When the host encounters import('remoteApp/Button'), Webpack injects a small bootstrap that fetches remoteEntry.js from the remote origin. That entry script contains a manifest of exposed modules and the shared‑dependency negotiation logic. The remote then streams the requested chunk (the Button component plus any of its own dependencies that aren’t already satisfied by the host’s shared copies). The network panel will show a single additional request, keeping the initial host bundle lean.
Trade‑offs and limitations
- Remote availability – If the remote server is down, the dynamic import rejects. A common mitigation is to wrap the import in a
try/catchand fall back to a local stub or a cached version. - Version alignment – Both containers must run the same major Webpack version (5.x) and compatible loader setups. Mismatched
sharedconfiguration can cause duplicate React instances or missing peer dependencies. - Cache invalidation – Because
remoteEntry.jsis requested at runtime, long‑term caching requires explicit versioned filenames or cache‑busting query strings. - Security – The host executes code served from another origin. Ensure CORS headers are set correctly and only trusted remotes are declared.
Quick verification checklist
- Both dev servers start without errors (look for “Module Federation” plugin messages).
- In the host page, open DevTools → Network → filter “JS”. Trigger the lazy import (e.g., click a button that calls
loadButton()). - Confirm a request to
http://localhost:8081/remoteEntry.jsappears, followed by the Button chunk. - Inspect the console: no “Uncaught (in promise)” errors, and the component renders.
If any step fails, double‑check the publicPath: 'auto' setting, CORS headers on the remote dev server, and that the shared keys match exactly (case‑sensitive).
Next steps
Start by extracting a single low‑risk UI component into a remote. Measure the host’s initial bundle size before and after (Webpack’s --json output or webpack-bundle-analyzer). Once comfortable, expand the exposes map and refine the shared strategy (e.g., requiredVersion, strictVersion) to match your release cadence. Module Federation turns micro‑frontend theory into a concrete Webpack workflow—give it a spin on a feature branch and see the network payload shrink.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.