Avoiding Bundle Duplication #
Duplicate dependencies across independently deployed micro-frontends inflate initial load times and introduce severe runtime conflicts. This guide details the configuration patterns required to centralize shared modules, enforce singleton constraints, and maintain version consistency without sacrificing team autonomy. Effective deduplication is a foundational requirement when evaluating Core Micro-Frontend Architecture & Tradeoffs.
Key Implementation Objectives:
- Identify framework-level dependencies (React, Vue, Angular) that must remain strict singletons.
- Configure Webpack’s
sharedobject with explicit semver ranges andsingleton: true. - Establish a centralized version policy to prevent runtime mismatches across independently deployed remotes.
- Validate deduplication through bundle analysis and runtime
shareScopeinspection.
Setup/Config #
The baseline for dependency sharing resides in the ModuleFederationPlugin configuration. Properly scoping the shared property dictates how Webpack resolves packages at build time versus runtime.
Align dependency boundaries early by Defining Application Boundaries before implementing shared scopes. This prevents accidental leakage of application-specific utilities into the global dependency graph.
Configuration Guidelines:
- Explicit Package Mapping: Define each shared package individually rather than relying on auto-discovery. This provides granular control over resolution behavior.
- Framework Singletons: Set
singleton: truefor UI frameworks. This guarantees only one instance loads into memory, regardless of how many remotes request it. - Semver Enforcement: Use
requiredVersionpaired withstrictVersion: true. At build time, Webpack validates the host’s installed version against the remote’s requirement. At runtime, if the versions mismatch, Webpack will either reject the shared instance or force an isolated fallback. - Lazy Loading: Keep
eager: false(default). Settingeager: trueforces Webpack to bundle the dependency into the host’s initial chunk, completely negating runtime deduplication and increasing payload size.
// webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'hostApp',
filename: 'remoteEntry.js',
shared: {
react: {
singleton: true,
requiredVersion: '^18.2.0',
strictVersion: true,
eager: false
},
'react-dom': {
singleton: true,
requiredVersion: '^18.2.0'
},
'@shared/ui-kit': {
requiredVersion: '^2.1.0',
singleton: false // UI components may safely load multiple instances
}
}
})
]
};
Runtime vs Build-Time Implication: strictVersion: true enforces validation during the Webpack compilation phase. If a remote’s package.json specifies a version outside the host’s requiredVersion range, the build will fail, preventing incompatible deployments. At runtime, singleton: true ensures the first loaded instance populates the shareScope, and subsequent requests reference that exact memory pointer.
Integration #
Once configured, the runtime initialization sequence must be strictly ordered to activate shared scopes before consuming remote modules.
Implementation Steps:
- Initialize Sharing: Call
__webpack_init_sharing__('default')immediately on application bootstrap. This populates the global share scope with the host’s available dependencies. - Container Initialization: Invoke
container.init(__webpack_share_scopes__.default)on the remote before requesting modules. This allows the remote to negotiate which dependencies to borrow from the host. - Dynamic Resolution: Use the
get()method exposed by the remote container to fetch modules. Wrap this inasync/awaitto handle network latency and version negotiation. - Fallback Handling: Implement try/catch blocks around
get(). If version resolution fails, gracefully degrade to an isolated fallback bundle. - Cross-Team Coordination: Manage dependencies through explicit API contracts rather than implicit state sharing, which directly impacts Managing Cross-Team Coupling.
- Scope Isolation: Configure custom
shareScopenames (e.g.,'experimental') to isolate beta features from production dependencies.
// remoteLoader.js
async function loadRemote(remoteName, modulePath) {
// 1. Initialize host sharing scope
await __webpack_init_sharing__('default');
// 2. Load remote container script (assumes already injected via <script> or dynamic import)
const container = window[remoteName];
// 3. Initialize remote with host's share scope
await container.init(__webpack_share_scopes__.default);
// 4. Request module
const factory = await container.get(modulePath);
return factory();
}
// Graceful degradation on version mismatch
async function safeLoadComponent() {
try {
return await loadRemote('remoteApp', './CheckoutComponent');
} catch (err) {
console.warn('Shared scope resolution failed, loading isolated fallback');
return loadIsolatedFallback();
}
}
Runtime vs Build-Time Implication: The initialization sequence occurs entirely at runtime. Webpack generates the negotiation logic during the build phase, but the actual dependency resolution happens when the browser executes container.init(). If __webpack_init_sharing__ is skipped, the remote cannot access the host’s dependencies and will load its own copies, causing duplication.
Edge Cases #
Version conflicts, peer dependency mismatches, and dynamic import failures require defensive programming patterns.
Resolution Strategies:
- Semver Gaps: When
requiredVersionrejects a compatible minor version due to overly strict ranges, adjust the host’spackage.jsonto align with the lowest common denominator across all remotes. - Custom Version Resolvers: Implement runtime inspection of
__webpack_share_scopes__to programmatically select the optimal version when Webpack’s default resolver fails. - Major Version Migration: Prevent framework splits (e.g., React 17 to 18) by deploying a transitional host that exposes both versions under different
shareScopekeys, then migrate remotes incrementally. - Graceful Degradation: When a remote requests a dependency absent from the host’s scope, Webpack automatically loads the remote’s bundled copy. Ensure your fallback UI can handle this isolated state without breaking.
- Intentional Multi-Instance Libraries: Isolate third-party UI libraries that require multiple instances (e.g., legacy date pickers or charting libraries) by setting
singleton: falseand scoping them to specific remotes.
// customResolver.js
const semver = require('semver');
function resolveSharedVersion(pkgName) {
const shareScope = __webpack_share_scopes__.default;
const versions = Object.keys(shareScope[pkgName] || {});
if (versions.length === 0) return null;
// Filter out invalid entries and sort descending
const validVersions = versions.filter(v => semver.valid(v));
return validVersions.sort((a, b) => semver.compare(b, a))[0];
}
// Usage: Inspect active scope before initialization
const reactVersion = resolveSharedVersion('react');
if (reactVersion && !semver.satisfies(reactVersion, '^18.0.0')) {
console.warn(`Host React ${reactVersion} does not satisfy remote requirements.`);
}
Runtime vs Build-Time Implication: Custom resolvers operate purely at runtime. They bypass Webpack’s static analysis and allow dynamic fallback logic. Use them sparingly, as they add execution overhead and complicate debugging.
Testing/Validation #
Deduplication must be verified through both static analysis and runtime inspection to prevent silent regressions.
Validation Workflow:
- Bundle Analysis: Run
webpack-bundle-analyzerin CI. Confirm that shared dependencies are extracted into distinct chunks (e.g.,vendors-node_modules_react_*.js) and not duplicated in host or remote entry points. - Runtime Assertion: Assert that
__webpack_share_scopes__.defaultcontains exactly one instance of each marked singleton. Log the scope object in development mode to verify resolution paths. - Automated Size Budgets: Integrate
webpack-bundle-sizeor custom CI scripts to enforce maximum payload thresholds. Fail builds if shared chunks exceed expected sizes. - Performance Tracking: Track runtime performance metrics using Measuring bundle size impact of shared dependencies to correlate deduplication with Core Web Vitals.
- Network Simulation: Use browser dev tools to throttle network speed and simulate failed remote loads. Validate that fallback mechanisms trigger without crashing the host application.
Deployment #
Shared dependency updates require coordinated rollout procedures to prevent production outages.
Rollout Procedures:
- Phased Upgrades: Coordinate major dependency upgrades using feature flags and canary deployments. Never update the host’s shared framework version without verifying remote compatibility first.
- Cache Strategy: Configure
Cache-Control: public, max-age=31536000, immutablefor shared chunks. Since Webpack hashes filenames, long-term caching is safe. Version remote containers independently to allow isolated updates. - Health Checks: Implement pre-deployment health checks that verify
shareScopeconsistency. Route traffic only after confirming all remotes successfully initialize against the new host version. - Rollback Triggers: Define automated rollback thresholds when version resolution failures exceed 1% of user sessions within a 5-minute window.
- Dependency Matrices: Maintain a centralized, version-controlled matrix documenting required
sharedversions for each remote. Prevent deployment-time incompatibilities by blocking CI/CD pipelines that violate the matrix.
Common Pitfalls #
| Issue | Root Cause | Resolution |
|---|---|---|
| Multiple React instances causing hook errors | Omitting singleton: true or misconfiguring requiredVersion allows multiple React copies to load, triggering “Invalid Hook Call” warnings and breaking component state. |
Enforce singleton: true and strictVersion: true identically across all host and remote configurations. |
| Eager loading shared dependencies defeats deduplication | Setting eager: true forces Webpack to bundle shared packages into the host’s initial chunk, negating the benefits of runtime sharing and increasing initial payload size. |
Keep eager: false (default) to ensure dependencies are loaded asynchronously via the shared scope. |
| Semver range mismatches causing silent fallbacks | Using overly broad ranges (e.g., * or >=1.0.0) allows incompatible major versions to resolve, leading to runtime API mismatches that bypass Webpack’s strict version checks. |
Use caret ranges (^) matching the exact major.minor version and enable strictVersion: true. |
| Missing shareScope initialization order | Calling container.get() before __webpack_init_sharing__() completes results in undefined shared modules and forces the remote to load its own isolated copies. |
Always await __webpack_init_sharing__('default') and container.init() before invoking get(). |
FAQ #
How do I prevent duplicate React instances in Module Federation?
Set singleton: true and strictVersion: true in the shared config for both host and remote apps. This ensures Webpack validates versions at build time and resolves to a single memory instance at runtime.
What happens if a remote app requires a dependency version outside the host’s range?
Webpack will either throw a version mismatch error during initialization or load the dependency in isolation, depending on your strictVersion and fallback configurations. Always implement graceful degradation to prevent application crashes.
Should I mark all dependencies as singletons? No. Only mark framework-level packages (React, Vue, Angular, Redux, etc.) as singletons. Utility libraries, CSS-in-JS engines, and UI components should typically allow multiple instances to avoid breaking changes and memory leaks.
How can I verify that bundle deduplication is working in production?
Inspect __webpack_share_scopes__.default in the browser console to confirm a single instance per package. Use webpack-bundle-analyzer in CI to validate chunk extraction, and monitor network requests to ensure shared chunks are fetched only once across all remotes.