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:

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:

// 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:

  1. Initialize Sharing: Call __webpack_init_sharing__('default') immediately on application bootstrap. This populates the global share scope with the host’s available dependencies.
  2. 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.
  3. Dynamic Resolution: Use the get() method exposed by the remote container to fetch modules. Wrap this in async/await to handle network latency and version negotiation.
  4. Fallback Handling: Implement try/catch blocks around get(). If version resolution fails, gracefully degrade to an isolated fallback bundle.
  5. Cross-Team Coordination: Manage dependencies through explicit API contracts rather than implicit state sharing, which directly impacts Managing Cross-Team Coupling.
  6. Scope Isolation: Configure custom shareScope names (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:

// 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:

Deployment #

Shared dependency updates require coordinated rollout procedures to prevent production outages.

Rollout Procedures:

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.