Eager vs Lazy Shared Module Loading #

eager: true is the setting most often changed to make an error go away and least often understood — this page explains what it actually does, which side of the graph should use it, and why the eager-consumption error is usually telling you something else entirely.

The short version: exactly one participant should register a shared dependency eagerly, every other side should request it lazily, and the famous “Shared module is not available for eager consumption” error is almost never fixed by setting eager: true. It extends managing shared dependencies at runtime.

What eager actually changes The setting decides when a shared module loads and where its bytes live; it has no bearing on how many copies exist at runtime. Property eager: true eager: false Where the module lands initial chunk separate fallback chunk When it registers at container init on request from the scope Entry size includes the dependency a reference only Fallback chunk none present, usually unused Async boundary needed no yes
The last row is why the setting gets misused: turning eager on removes the async requirement, and the error goes away for the wrong reason.

Prerequisites #

Step 1 — Understand what eager actually changes #

eager decides whether a shared module is included in the initial chunk or in a separate chunk loaded on demand.

With eager: false — the default — webpack emits a consume-shared reference and a fallback chunk. At runtime the module asks the share scope for the dependency; only if the scope cannot satisfy it does the fallback chunk load. That request is asynchronous, which is why the entry must not evaluate shared modules synchronously.

With eager: true, the dependency is compiled into the initial chunk and registered into the share scope as soon as the container initialises. There is no asynchronous step and no fallback chunk.

The consequence follows directly: eager registration makes the dependency available to everyone immediately and puts its full weight in the entry. Lazy consumption keeps the entry small and requires that nothing touches the dependency before the scope is initialised.

Step 2 — Assign eager by role, not by trial and error #

Who registers and who requests Exactly one participant registers a shared dependency eagerly; every other side requests it and receives the already-registered instance. One eager registrar, many lazy consumers Host — eager first code to run · registers react into the scope Share scope populated before any remote asks · one version, one instance Remote A — lazy requests from the scope · fallback chunk never fetched Remote B — lazy requests from the scope · same instance as the host
Invert this and every remote entry ships the framework — which is the duplication sharing was configured to prevent.

There is a correct answer, and it depends on what the build is.

The host — the shell that boots the application — should register shared dependencies eagerly. It is the first code to run, it is going to load React regardless, and eager registration means the scope is populated before any remote asks.

Remotes consumed by a host should be lazy. They request from the scope and normally receive the host’s copy, so shipping their own would be paying twice for something they will not use.

// host/webpack.config.js — the host owns the registration
shared: {
  react: { singleton: true, requiredVersion: deps.react, eager: true },
  'react-dom': { singleton: true, requiredVersion: deps['react-dom'], eager: true },
}
// remote/webpack.config.js — remotes consume, they do not register
shared: {
  react: { singleton: true, requiredVersion: deps.react, eager: false },
  'react-dom': { singleton: true, requiredVersion: deps['react-dom'], eager: false },
}

A remote that also runs standalone still uses eager: false. Its fallback chunk covers the standalone case — that is exactly what the fallback exists for — and it is not downloaded when a host has already registered a satisfying version.

Step 3 — Fix eager consumption at its actual cause #

The error reads: Shared module is not available for eager consumption. It means something imported a shared module synchronously from the entry, before the share scope was initialised.

Setting eager: true makes it disappear by removing the asynchronous step, which is why it is such a tempting fix. It also ships that dependency in every remote’s entry, which is the duplication you configured sharing to avoid.

The correct fix is the async boundary:

// src/index.js — this file imports nothing else
import('./bootstrap');
// src/bootstrap.js — everything real lives here
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';

createRoot(document.getElementById('root')).render(<App />);

The dynamic import creates a chunk boundary. Webpack initialises the share scope, then evaluates bootstrap.js, by which point the shared modules resolve normally. Two files, no configuration change, and the entry stays small.

Step 4 — Find the import that is still synchronous #

Diagnosing the eager-consumption error Two of the three answers cost nothing at runtime; only the third ships duplicated bytes, and it applies to a narrow class of dependency. Shared module is not available for eager consumption. What now? Entry imports app code directly Add the async boundary index.js does only import('./bootstrap') Correct fix, no config change Boundary exists, error persists Move the stray import A polyfill or setup file reaches it first Stats reasons name the importer Must run before any await Set eager, deliberately Polyfills patching globals Accept the duplicated weight
Reaching for the third branch first is the most common way a federated graph acquires duplicate frameworks.

When the error persists despite an async boundary, something is reaching a shared module before it.

Common culprits: a polyfill import at the top of the entry, a side-effecting CSS-in-JS setup, an analytics snippet importing a shared utility, or a setupTests-style file accidentally included in the production entry.

// Find which module pulled the shared dependency into the initial chunk.
const stats = JSON.parse(readFileSync('stats.json', 'utf8'));
const initial = stats.chunks.filter((c) => c.initial);

for (const chunk of initial) {
  for (const mod of chunk.modules ?? []) {
    if (!mod.name?.includes('node_modules/react/')) continue;
    console.log('reached from:', mod.reasons?.map((r) => r.moduleName).join(', '));
  }
}

The reasons array names the importer, which is usually enough to identify the file. Move that import inside bootstrap.js and the error resolves without touching the shared configuration.

Step 5 — Recognise the cases where eager is genuinely right #

There are three, and they are narrower than the setting’s popularity suggests.

A host that is the sole entry point, as in step 2 — the standard case.

A dependency that must be present before any application code runs, such as a polyfill that patches a global. There is no way to await a share scope before a polyfill applies, so eager is the only option; the cost is that every side shipping it eagerly ships a copy.

A remote deployed standalone as its own application, where there is no host to register anything. Note that this is a property of the deployment, not the remote, which is why the standalone entry rather than the exposed module is where it belongs.

// A polyfill that must apply before anything else. Eager on every side.
shared: {
  'core-js': { singleton: true, eager: true, requiredVersion: deps['core-js'] },
}

Step 6 — Confirm what the build actually produced #

The configuration states intent; the output is the fact.

# A lazily-consumed dependency should NOT be in the initial chunk.
npx webpack --json > stats.json
node -e "
  const s = require('./stats.json');
  const initial = s.chunks.filter(c => c.initial).flatMap(c => c.modules ?? []);
  const bundled = initial.filter(m => /node_modules\/react\//.test(m.name ?? ''));
  console.log(bundled.length ? 'react is EAGER in the initial chunk' : 'react is lazily consumed');
"

Run this in each remote’s CI. A remote that silently flips to eager — because someone added an import, or changed the configuration to clear an error — is a duplication regression that no size budget on its own will explain, and it is the assertion described in analyzing federated bundles with webpack-bundle-analyzer.

Step 7 — Understand the interaction with singleton and strictVersion #

The three settings are independent and are routinely confused.

singleton: true means only one instance may be used, and a mismatch produces a warning rather than a second copy. strictVersion: true upgrades that mismatch from a warning to an error. eager decides only when the module is loaded, not how many copies exist.

The combination worth defaulting to for framework dependencies is singleton: true, strictVersion: true, with eager set by role. That gives one instance, a loud failure if versions cannot be reconciled, and no duplicated weight in remote entries.

// The framework contract, expressed once and mirrored everywhere.
const framework = (version, eager) => ({
  singleton: true, requiredVersion: version, strictVersion: true, eager,
});

// host:   shared: { react: framework(deps.react, true) }
// remote: shared: { react: framework(deps.react, false) }

Sharing this helper from one internal package is worth doing. The failures in this area come overwhelmingly from configurations that were meant to match and quietly diverged.

Verification #

Troubleshooting #

Symptom: eager consumption error despite an async bootstrap.

Diagnosis: another import in the entry reaches a shared module first. Fix: use the stats analysis in step 4 to name the importer, then move it inside the bootstrap module.

Symptom: every remote entry is large and contains React.

Diagnosis: eager: true was set on remotes to clear an error. Fix: restore the async boundary, set remotes back to lazy, and assert on the initial chunk in CI.

Symptom: a remote works standalone and fails inside the host.

Diagnosis: the standalone entry’s eager registration is leaking into the federated build. Fix: keep the standalone entry out of exposes and out of the federated entry chunk.

Symptom: the fallback chunk downloads on a composed page.

Diagnosis: negotiation failed, so the remote fell back to its own copy. Fix: align versions and enable strictVersion so the mismatch fails loudly instead of silently duplicating.