Published: April 17, 2026 · 14 min read
Dynamic Remote Loading in Module Federation
You have host and remote apps configured with shared dependencies and everything works in local development. Then you need to deploy. The remote URLs in your webpack config point to https://localhost:4002 — but production serves from /products/. You rebuild the host with production URLs, deploy, and move on. Next week, the staging environment needs different URLs. Another rebuild. A new remote team onboards — another rebuild of the host just to add their URL. Dynamic remote loading solves this by resolving remote URLs at runtime instead of baking them into the webpack build.
This is Article 15 in the Micro Frontend Architecture series. If you haven't configured shared dependencies yet, start with Module Federation Shared Dependencies.

The Problem with Static Remotes
In every article so far, remote URLs have been hardcoded strings inside webpack.config.js. This is called static remote loading — Webpack bakes the URL into the build output at compile time.
This works fine when you have one environment and a few remotes. But in real projects with multiple environments (local, staging, production) and multiple teams deploying independently, static remotes create a bottleneck: every URL change requires rebuilding and redeploying the host application. The host becomes the release gatekeeper for every remote team.
Static remotes also block independent deployments. If the Products team deploys a new version to a different URL (e.g., a versioned CDN path), the host must be rebuilt to pick up the new URL. This defeats one of the core benefits of micro frontend architecture — independent team deployments.
What is Dynamic Remote Loading?
Dynamic remote loading resolves remote URLs at runtime instead of at build time. Instead of hardcoding "Products@https://localhost:4002/remoteEntry.js" in the webpack config, you load the URL from a configuration file, an environment variable, or an API call when the application starts in the browser.
This means:
- The same host build works in local, staging, and production — only the config changes
- New remotes can be added without rebuilding the host
- Remotes can be feature-flagged — disabled or enabled at runtime
- Each remote team can deploy independently to versioned URLs
- A/B testing becomes possible — different users can load different remote versions
How Dynamic Remote Loading Works
Dynamic remote loading follows a three-step process that mirrors what Webpack does internally with static remotes — but you control when and how each step happens.

Step 1: Load the Remote Entry Script
The first utility loads a remote's remoteEntry.js file by dynamically injecting a <script> tag. When the script executes, it registers the remote container as a global variable on window (e.g., window.Products).
Step 2: Load the Component from the Container
Once the remote container is available on window, the second utility initializes shared dependency negotiation and retrieves the specific exposed module.
__webpack_init_sharing__ and __webpack_share_scopes__ are Webpack runtime globals. They exist automatically in any application built with ModuleFederationPlugin (opens in a new tab). You do not need to import or declare them — Webpack injects them at build time. These globals power the shared dependency system covered in the previous article.
Step 3: Combine with Caching
The third utility wraps both steps into a single function with a cache to prevent re-fetching the same module on subsequent renders or navigation.
Environment-Based Remote Configuration
The most common use case for dynamic remote loading is environment-based configuration — using different remote URLs for local development, staging, and production without rebuilding the host.
The resolver picks the correct config at runtime based on the environment.
Now the host's App.jsx uses the resolver to create lazy components that load from the right URLs automatically.
The createDynamicRemote factory replaces static React.lazy() imports. Instead of lazy(() => import("Products/ProductList")) which requires webpack to know the remote URL at build time, the factory resolves the URL at runtime from the config. The rest of the pattern — Suspense, ErrorBoundary, route wrapping — stays identical to the host and remote setup.
Promise-Based Remotes in Webpack Config
Webpack Module Federation supports an alternative approach: promise-based remotes directly in the webpack config. Instead of a static URL string, you pass a promise that resolves to the remote container at runtime.
This approach keeps dynamic loading inside the webpack config without requiring separate utility files. However, the config becomes harder to read and debug. For most projects, the utility-based approach (loadRemoteEntry + loadComponent) is cleaner.
Remote Registry Pattern
For large-scale projects with many remotes and multiple teams, an API-driven remote registry provides centralized control over which remote versions are loaded.
The registry pattern enables:
- Versioned deployments — each remote has a version in its URL, so the backend can roll back to a previous version by changing the URL
- Gradual rollouts — serve the new version to 10% of users, then 50%, then 100%
- Instant rollbacks — if a remote breaks in production, update the registry to point back to the previous version — no host rebuild needed
Feature Flags and Conditional Remote Loading
Combining the remote registry with feature flags lets you enable or disable entire micro frontends at runtime.
This is powerful for:
- Gradual feature releases — enable a new MFE for internal users first, then beta users, then everyone
- Kill switches — instantly disable a broken remote without deploying anything
- Region-based features — load different remotes based on user geography
Error Handling and Retry Strategies
Dynamic loading introduces more failure points than static loading — network timeouts, CDN outages, DNS resolution failures. Retry logic with exponential backoff handles transient failures gracefully.
The retry pattern is especially important in production where network conditions are unpredictable. A single failed fetch should not permanently break a page — the user might be on a slow connection that recovers after one retry.
Preloading Remote Entries for Performance
Dynamic loading adds a small overhead: the browser must fetch remoteEntry.js when the user first navigates to a remote's route. You can eliminate this delay by preloading remote entries during browser idle time.
Preloading works best for remotes the user is likely to visit. Analyze your navigation patterns — if 80% of users visit Products after logging in, preload the Products remote entry during idle time after the login page renders.
Security: Validating Remote Origins
Dynamic remote loading loads and executes arbitrary JavaScript from URLs resolved at runtime. Without validation, a compromised config or registry could inject a malicious remoteEntry.js. Always validate remote URLs against a whitelist of trusted origins.
Never load remote entries from user-provided URLs. Remote entry scripts execute immediately when the <script> tag loads — they have full access to the page's DOM, cookies, and JavaScript context. Always validate against a hardcoded list of trusted origins. If your registry API is compromised, the origin validation acts as a second line of defense.
Dynamic Remote Loading in Next.js
Next.js uses NextFederationPlugin (opens in a new tab) instead of ModuleFederationPlugin, and the remote entry paths differ — Next.js serves them from _next/static/chunks/remoteEntry.js (client) and _next/static/ssr/remoteEntry.js (server).
Key Differences from React
| Aspect | React (Webpack) | Next.js |
|---|---|---|
| Plugin | ModuleFederationPlugin | NextFederationPlugin |
| Loading method | React.lazy() | next/dynamic with ssr: false |
| Remote entry path | /remoteEntry.js | /_next/static/chunks/remoteEntry.js |
| SSR support | Not applicable | isServer check for SSR/client paths |
| Config file | webpack.config.js | next.config.js |
| Shared version | requiredVersion: "^18.3.1" | requiredVersion: false |
Static vs Dynamic Remotes — When to Use Each

| Feature | Static Remotes | Dynamic Remote Loading |
|---|---|---|
| URL resolution | Build time (webpack.config.js) | Runtime (config file or API) |
| Environment switch | Rebuild host per environment | Change config — no rebuild needed |
| Adding a new remote | Modify webpack config + rebuild host | Add entry to config file or registry |
| Feature flags | Not possible without rebuild | Toggle enabled/disabled at runtime |
| Versioned deployments | All remotes deploy at once | Each remote can deploy independently |
| A/B testing | Not possible | Load different remote URLs per user cohort |
| Complexity | Simple — one config file | Higher — loader utils, config management |
| Initial load performance | Slightly faster (no config fetch) | ~10-50ms overhead for config resolution |
| Error surface | Webpack catches missing remotes at build | Runtime errors — need robust error handling |
| When to use | Small team, few remotes, single deploy | Multiple teams, many remotes, CI/CD per MFE |
Use static remotes when you have a small team, a handful of remotes, and a single deployment pipeline. The simplicity is worth more than the flexibility.
Use dynamic remote loading when you have multiple teams deploying independently, multiple environments with different remote URLs, or requirements for feature flags and gradual rollouts. The added complexity pays for itself in deployment flexibility.
What's Next?
← Back to Module Federation Shared Dependencies
Continue to Lazy Loading MFE Components with React Suspense →
Frequently Asked Questions
What is the difference between static and dynamic remote loading in Module Federation?
Static remote loading hardcodes remote URLs in webpack.config.js at build time — changing the URL requires rebuilding the host application. Dynamic remote loading resolves remote URLs at runtime using configuration files, environment variables, or API calls. This means the same host build can point to different remote URLs in development, staging, and production without rebuilding. Dynamic loading also enables feature flags, A/B testing, and independent deployments where each remote team deploys on their own schedule.
How do I load a remote micro frontend dynamically at runtime?
Dynamic remote loading follows three steps. First, inject a <script> tag with the remote's entry URL to load its remoteEntry.js file — this registers the remote container on the window object. Second, call __webpack_init_sharing__("default") to initialize the shared dependency scope, then call container.init(__webpack_share_scopes__.default) to let the remote negotiate shared dependencies with the host. Third, call container.get("./ModuleName") to retrieve the module factory, then execute the factory to get the actual component. Wrapping these steps in a utility function and combining it with React.lazy() gives you a clean dynamic loading pattern.
Can I change remote URLs without rebuilding the host application?
Yes. With dynamic remote loading, remote URLs come from runtime configuration instead of the webpack build. You can use environment-specific config files (remotes.local.js, remotes.production.js), inject URLs via a <script> tag in index.html (window.__REMOTE_CONFIG__), or fetch URLs from an API endpoint at startup. Each approach lets you deploy the same host build to any environment — only the configuration changes. The remote registry pattern takes this further by letting a backend API control which remote versions are loaded.
How do I handle errors when a dynamic remote fails to load?
Dynamic remote loading can fail for several reasons — network errors, remote server downtime, or version mismatches. Build multiple layers of protection: use .catch() on the lazy import to return a fallback component, wrap remote components in React ErrorBoundary to catch render-time errors, implement retry logic with exponential backoff (1s, 2s, 4s delays) for transient network failures, and validate remote origins before loading scripts to prevent loading from untrusted sources. The combination of graceful fallbacks and retry logic ensures the host application stays functional even when individual remotes are unavailable.
What is the promise-based remote pattern in Webpack Module Federation?
The promise-based remote pattern replaces the static remote string in webpack.config.js with a promise that resolves to the remote container at runtime. Instead of writing Products: "Products@/products/remoteEntry.js", you write Products: "promise new Promise(...)" where the promise creates a <script> tag, loads the remote entry, and resolves with a proxy object that delegates get() and init() calls to the actual container on window. This approach keeps dynamic loading inside the webpack config without requiring utility functions, but it makes the config harder to read.
Is dynamic remote loading slower than static remote loading?
Dynamic remote loading adds 10-50ms of overhead for configuration resolution (reading a config file or fetching from an API). The actual remote entry loading and module resolution take the same time as static loading — the browser still fetches remoteEntry.js and the component bundle. You can minimize the overhead by caching the configuration, preloading remote entries during browser idle time using requestIdleCallback, and caching loaded modules to prevent re-fetching on navigation. In practice, the overhead is imperceptible to users and is a worthwhile trade-off for the deployment flexibility it provides.