Managing Shared Dependencies at Runtime #
Runtime dependency management is the backbone of scalable micro-frontend architectures. This guide details how to configure shared modules, enforce singleton patterns, and handle version negotiation without bloating host or remote bundles.
Key Implementation Priorities:
- Singleton enforcement prevents duplicate framework instances across micro-apps
- Version fallback strategies ensure graceful degradation when exact matches fail
- Build-time declarations must align with runtime resolution logic
- Cross-host dependency loading requires strict CORS and caching alignment
Setup/Config: Defining Runtime Shared Modules #
Establishing a robust shared configuration dictates how libraries are resolved, cached, and instantiated at runtime. Architects must align build-time declarations with execution contexts, building upon core architectural patterns established in Webpack & Vite Module Federation Implementation. The shared configuration acts as a contract between the build pipeline and the browser’s module loader.
Configuration Directives:
singleton: true: Mandatory for framework-level dependencies (React, Vue, Angular). Prevents multiple isolated contexts that break global state and context providers.requiredVersion: Enforce strict semver ranges (e.g.,^18.2.0) to prevent silent downgrades or incompatible major version loads.eager: false: Defers loading until the remote module is actually requested, preserving initial bundle size and TTI metrics.
For Webpack-specific overrides and advanced chunk splitting, reference detailed syntax patterns in Configuring Webpack Module Federation. Teams leveraging alternative build tools should consult Setting Up Vite with Federation Plugins for equivalent plugin configurations that map to the same runtime resolution engine.
Integration: Cross-Host Dependency Resolution #
Dynamic import routing and runtime dependency negotiation require explicit mapping between host and remote applications. The goal is to trigger on-demand dependency fetching while maintaining strict module exposure boundaries to prevent circular dependency loops.
Integration Workflow:
- Map remote entry points to local import aliases for seamless consumption.
- Implement fallback loaders or secondary remote endpoints when primary URLs fail.
- Leverage dynamic
import()syntax to defer resolution until component mount. - Validate module boundaries during CI to ensure exposed APIs match consumer expectations.
Edge Cases: Version Conflicts & Peer Dependency Mismatches #
Runtime failures frequently stem from incompatible dependency trees and framework version skew. Diagnosing these issues requires inspecting the runtime share scope and implementing manual arbitration when automatic negotiation fails.
Troubleshooting & Resolution:
- Analyze
__webpack_share_scopes__in the browser console to inspect active dependency versions and instantiation counts. - Implement custom
getandinitlifecycle hooks for manual version arbitration when semver ranges conflict. - Isolate incompatible peer dependencies using scoped module aliases to prevent global scope pollution.
- Deploy runtime guards to detect mismatched context providers before hydration begins.
For React-specific ecosystem failures, deep-dive troubleshooting is covered in Resolving version conflicts in shared React libraries, while broader ecosystem issues are addressed in Handling peer dependency mismatches in Module Federation.
Testing/Validation: Runtime Dependency Simulation #
Automated validation pipelines must verify dependency resolution under network constraints and simulated version drift. Infrastructure teams must account for Handling cross-origin resource sharing for remote modules during integration testing to ensure remote chunks execute without browser security blocks.
Validation Checklist:
- Mock remote entry points in test environments to verify fallback resolution paths.
- Inject version overrides via environment variables (
SHARED_REACT_VERSION=17.0.2) to simulate dependency drift. - Validate singleton instantiation counts using browser DevTools memory profiling and heap snapshots.
- Automate contract testing for exposed module APIs across staging and production deployment pipelines.
Deployment: Production Hardening & Caching Strategies #
Optimizing delivery pipelines ensures consistent dependency availability, predictable cache invalidation, and secure remote module execution.
Production Directives:
- Configure
Cache-Control: public, max-age=31536000, immutableheaders for versioned shared chunks. - Implement content-hash-based filenames (e.g.,
react.8f3b2c.js) to force cache busting on updates. - Deploy edge-side includes or CDN routing for high-availability remote entry distribution.
- Monitor runtime dependency load times and fallback triggers via Real User Monitoring (RUM) metrics.
Implementation Reference #
Webpack 5 Shared Configuration with Singleton Enforcement #
// webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host_app',
remotes: { remote_app: 'remote_app@http://localhost:3001/remoteEntry.js' },
shared: {
react: { singleton: true, requiredVersion: '^18.2.0', eager: false },
'react-dom': { singleton: true, requiredVersion: '^18.2.0', eager: false },
lodash: { singleton: false, requiredVersion: '^4.17.21' }
}
})
]
};
Runtime vs Build-Time Implication: Build-time, Webpack statically analyzes package.json to validate semver ranges. At runtime, the singleton: true flag forces the first loaded version into the global share scope. Subsequent requests for react will reuse the cached instance, preventing duplicate framework initialization.
Vite Federation Plugin Shared Config #
// vite.config.js
import { defineConfig } from 'vite';
import { federation } from '@module-federation/vite';
export default defineConfig({
plugins: [
federation({
name: 'host_app',
remotes: { remote_app: 'http://localhost:3001/remoteEntry.js' },
shared: {
react: { singleton: true, version: '^18.2.0' },
'react-dom': { singleton: true, version: '^18.2.0' }
}
})
]
});
Runtime vs Build-Time Implication: Vite’s federation plugin translates version constraints into runtime negotiation rules during the dev server and build phases. Unlike Webpack’s eager chunking, Vite defers shared module resolution until dynamic import execution, aligning build-time declarations with native ES module loading behavior.
Runtime Version Negotiation Hook #
// shared-loader.js
export async function loadSharedDependency(scope, module) {
// Initialize the default share scope at runtime
await __webpack_init_sharing__('default');
// Access the runtime share scope to inspect available factories
const factory = __webpack_share_scopes__.default[module];
if (!factory) throw new Error(`Shared module ${module} not found in scope`);
// Arbitrate version selection and initialize the resolved module
const [version, init] = factory;
return init();
}
Runtime vs Build-Time Implication: Build-time configuration declares intent, but this hook executes at runtime. It bypasses automatic fallback logic, allowing developers to manually inspect __webpack_share_scopes__, log version mismatches, or trigger custom error boundaries before module initialization.
Common Pitfalls #
| Issue | Root Cause & Resolution |
|---|---|
| Silent Singleton Duplication | Omitting singleton: true causes multiple framework instances to load, triggering React context provider errors and state isolation failures. Fix: Explicitly enforce singletons for all stateful UI libraries. |
| Eager Loading Bundle Bloat | Setting eager: true forces shared dependencies into the host’s initial chunk, negating the performance benefits of lazy remote loading. Fix: Keep eager: false and rely on dynamic imports. |
| CORS Blocking Remote Chunks | Missing Access-Control-Allow-Origin headers on CDN origins prevents the browser from executing dynamically fetched remote entry scripts. Fix: Configure CDN edge rules to allow cross-origin script execution. |
| Strict Semver Rejection | Overly restrictive requiredVersion ranges cause runtime fallbacks to duplicate packages instead of sharing them, increasing memory overhead. Fix: Use compatible ranges (^ or ~) and validate across all remote package.json files. |
FAQ #
What happens if the remote app requires a newer version of a shared dependency than the host?
Module Federation attempts to satisfy the requiredVersion range. If the host’s version is incompatible, it loads a duplicate instance of the dependency from the remote. This may trigger singleton warnings and increase memory overhead if not properly isolated via scoped aliases or manual version arbitration.
Can I dynamically update shared dependencies without redeploying the host application? Yes, by configuring immutable caching and versioned remote entry points. The host resolves the latest compatible version at runtime, provided the remote exposes the updated chunk and CORS headers are correctly configured. Ensure content-hash filenames are used to prevent stale cache hits.
How do I debug why a shared module is loading twice?
Inspect __webpack_share_scopes__ in the browser console to verify active instances. Confirm singleton: true is set, ensure eager: false is applied, and check the network waterfall for duplicate chunk requests. Validate semver ranges across all remote and host package.json files to eliminate range mismatches.