Webpack vs Vite Module Federation Comparison: Architecture, Configs & Fixes

Webpack 5 and Vite implement module federation with fundamentally different machinery: Webpack injects a static runtime registry (__webpack_require__.S) at bundle time, while @originjs/vite-plugin-federation shims Rollup output and fetches remotes with native dynamic import(). That difference dictates how shared scope resolves, which errors you hit, and how you debug across boundaries. This guide gives reproducible configs and exact fixes for both; for the runtime sharing model both build on, start from Module Federation and Micro-Frontend Architectures.

Webpack versus Vite federation comparison matrix A two-column matrix contrasting Webpack 5 and Vite federation across architecture, shared scope, dev experience, and tree-shaking. Webpack 5 vs Vite federation Webpack 5 MF Vite plugin-federation Resolution bundle-time registry request-time import() Shared scope versioned registry flat shared map Dev mode dev-server, slower start build + preview only Tree-shaking conservative aggressive (needs sideEffects) Best for legacy CJS monoliths greenfield ESM apps Pick by constraint: runtime maturity vs dev velocity and ESM-native output
Figure: the load-bearing differences between Webpack and Vite federation, row by row.

Architectural Divergence: Bundle-Time vs Dev-Time Federation

Webpack constructs a complete module graph during compilation. It performs AST traversal to identify shared dependencies, hoists them into a shared chunk, and injects a custom runtime registry that intercepts import and require calls. This approach guarantees deterministic chunk boundaries and enables complex runtime sharing, but introduces eager evaluation overhead and tight coupling between host and remote build pipelines. The runtime maintains a global scope object (__webpack_require__.S) that tracks loaded versions, enforces singleton constraints, and resolves version mismatches at execution time.

Vite defers resolution to the browser. During development, the Vite dev server acts as an HTTP proxy, transforming bare module specifiers into absolute URLs and serving native ESM. Production builds use Rollup to pre-bundle and optimize, but the federation mechanism (via @originjs/vite-plugin-federation or @module-federation/vite) relies on standard import() and HTTP fetch rather than a custom runtime. This shifts dependency resolution from compile-time to request-time, aligning with modern browser capabilities and reducing build overhead.

Root Cause Analysis: Webpack’s eager shared scope frequently triggers version collisions when multiple remotes request mismatched peer dependencies. The runtime attempts to satisfy the first loaded version, breaking subsequent consumers that expect different major/minor releases. Vite’s native ESM graph isolates modules per origin, eliminating registry collisions but introducing strict CORS and base path alignment requirements. Understanding how modern bundlers abstract dependency resolution and chunk generation is critical when migrating between these paradigms. Refer to Core Concepts of Modern Bundling for foundational mechanics on graph construction, chunk splitting, and runtime injection strategies.

Configuration Paradigm Comparison:

  • webpack.ModuleFederationPlugin operates as a static runtime compiler. It generates remoteEntry.js with explicit chunk hashes and embeds a version negotiation algorithm directly into the bundle.
  • Vite implementations (@originjs/vite-plugin-federation or @module-federation/vite) operate as dynamic dev/proxy transformers. They rewrite import maps at request time, defer shared dependency loading, and rely on native ESM semantics rather than a custom registry.

Exact Configuration Patterns & Reproducible Setups

Side-by-side host/remote configurations require strict alignment of shared scope, version constraints, and ESM compatibility. Misalignment in either bundler results in runtime resolution failures or duplicate dependency injection.

Webpack 5: Static Shared Scope Resolution

Webpack’s shared configuration dictates how dependencies are hoisted, version-negotiated, and injected into the runtime registry. The plugin evaluates peerDependencies across the monorepo, applies semver constraints, and determines whether to fallback to the remote’s bundled copy or request the host’s instance.

// webpack.config.js (Remote or Host)  // Webpack 5.80+, Node 20+
const { ModuleFederationPlugin } = require("webpack").container;

module.exports = {
  output: { publicPath: "auto" }, // resolve chunk URLs relative to remoteEntry.js
  plugins: [
    new ModuleFederationPlugin({
      name: "remote_app",
      filename: "remoteEntry.js",
      exposes: { "./Component": "./src/Component.tsx" },
      shared: {
        react: { singleton: true, requiredVersion: "^18.0.0" },
        "react-dom": { singleton: true, requiredVersion: "^18.0.0" },
      },
    }),
  ],
};

Setting singleton: true prevents duplicate framework instances by forcing the host to provide the dependency. However, this breaks if requiredVersion mismatches the host’s installed version or if the remote attempts to load before the shared chunk initializes.

Exact Error: Module not found: Error: Can't resolve 'react' in shared scope Root Cause: Mismatched package.json peerDependencies or eager: true forcing synchronous resolution before the shared chunk loads. Webpack’s resolver fails to locate the dependency in the global scope because the host hasn’t registered it, or the remote’s package.json declares a conflicting range. Fix Steps:

  1. Set eager: false on shared dependencies to defer loading until runtime. This prevents synchronous resolution failures during initial chunk evaluation.
  2. Ensure host and remote peerDependencies use identical semver ranges. Run npm ls react across the workspace to verify hoisting consistency.
  3. Add resolve.alias for monorepo symlinked packages to bypass hoisting conflicts and point directly to the resolved node_modules path.

Vite: Native ESM & Plugin Federation

Vite federation relies on HTTP fetch for remote entries. The dev server must correctly expose the remoteEntry.js with appropriate headers, and the host must map remote URLs correctly without path rewriting conflicts. Note that @originjs/vite-plugin-federation is build-only — the remote must be built and served with vite preview, not vite dev.

// vite.config.ts (Remote)  // Vite 5.x, @originjs/vite-plugin-federation 1.3.x
import { defineConfig } from "vite";
import federation from "@originjs/vite-plugin-federation";

export default defineConfig({
  plugins: [
    federation({
      name: "remote_app",
      filename: "remoteEntry.js",
      exposes: { "./Component": "./src/Component.tsx" },
      shared: ["react", "react-dom"],
    }),
  ],
  build: { target: "esnext" }, // top-level await is required by the federation runtime
  server: {
    cors: true,
    origin: "http://localhost:5173",
  },
});

Exact Error: Failed to fetch dynamically imported module: http://localhost:5173/remoteEntry.js Root Cause: Missing CORS headers on the remote dev server or mismatched base configuration causing incorrect asset paths. Vite’s dev server defaults to strict origin policies, and the federation plugin expects exact path alignment between host and remote. Fix Steps:

  1. Enable server.cors: true and explicitly set server.origin in the remote vite.config.ts. This allows cross-origin fetch requests during development.
  2. Align base: '/' across host and remote configurations to prevent path rewriting during fetch. Misaligned bases result in 404s on remoteEntry.js or shared chunk requests.
  3. Verify remoteEntry.js is served with Content-Type: application/javascript. Misconfigured reverse proxies may return text/plain, causing the browser to reject module evaluation.

Micro-Frontend Integration & Shared Dependency Pitfalls

Shared scope behavior diverges significantly in production. Webpack merges shared modules into a single runtime chunk, while Vite maintains them as discrete ESM files. This architectural split directly impacts tree-shaking boundaries, hydration consistency, and concurrent mode compatibility.

When react-dom versions diverge across federated boundaries, Webpack’s global registry triggers hydration mismatches and state leakage. The runtime attempts to reconcile DOM nodes using mismatched reconciler algorithms, resulting in checksum failures and forced client-side rehydration. Vite’s native ESM imports prevent registry collisions but require explicit version negotiation layers to avoid duplicate framework loads across isolated origins. The full procedure for collapsing this to one instance is in sharing a singleton React instance across remotes.

Root Cause Analysis: Webpack’s runtime shares state via a global registry, causing hydration errors when react-dom versions diverge. Vite’s native ESM imports prevent registry collisions but require explicit version negotiation layers.

Fix Steps:

  1. Pin exact versions in the shared configuration and enable strictVersion: true to fail fast on mismatches. This prevents silent fallbacks to incompatible major versions.
  2. Validate host/remote compatibility by comparing installed React versions across packages before federation initialization: npm ls react in each workspace package.
  3. Use dynamic import() for lazy remote loading to avoid blocking the main thread during initial hydration. Defer remote evaluation until the host’s shared scope is fully initialized.

Step-by-Step Migration & Debugging Workflow

Transitioning from Webpack to Vite federation requires validating production builds, reconciling source maps, and auditing shared chunk boundaries. The migration path must account for differing tree-shaking algorithms, ESM/CJS interop layers, and runtime initialization sequences.

Resolving Production Build Failures

Vite’s default build.target may strip modern syntax required by older remote apps, causing runtime failures after migration. Rollup’s aggressive dead code elimination assumes unused imports are safe to drop, which breaks federated contracts when shared dependencies are incorrectly marked as external.

Exact Error: TypeError: Cannot read properties of undefined (reading 'createElement') Root Cause: Tree-shaking removed a shared dependency due to missing sideEffects: false or incorrect external configuration. Vite’s optimizer drops the dependency from the final chunk, leaving the remote with an undefined reference during component initialization. Fix Steps:

  1. Verify build.target matches the lowest supported browser ESM spec (e.g., esnext or chrome88). Use npx vite build --debug to inspect the Rollup configuration and confirm target transpilation.
  2. Disable minify temporarily (build.minify: false) to isolate scope collisions and verify chunk exports. Minification often obfuscates export names, breaking federation import maps.
  3. Use rollup-plugin-visualizer to audit shared chunk boundaries and confirm react/react-dom are not incorrectly externalized. Run npx vite build with the visualizer plugin and inspect the generated stats.html to verify dependency inclusion.

Debugging Federated Modules in Dev & Prod

Cross-boundary stack traces require aligned source map generation. Webpack uses devtool, while Vite relies on build.sourcemap and native browser mapping. Misaligned source maps obscure the origin of runtime errors, making it difficult to trace failures across host/remote boundaries.

// Vite config  // Vite 5.x
export default { build: { sourcemap: "inline" } };
// Webpack config  // Webpack 5.80+
module.exports = { devtool: "source-map" };

Fix Steps:

  1. Enable “Enable JavaScript source maps” in Chrome DevTools and disable “Pretty Print” to see original TSX/JSX lines. Inline source maps ensure accurate stack traces without external file requests.
  2. Use source-map-explorer on production bundles to verify chunk overlap and identify duplicated dependencies. Run npx source-map-explorer dist/assets/*.js to visualize import graphs and detect shared scope fragmentation.
  3. Add console.trace() in remote entry points (remoteEntry.js or initialization hooks) to isolate initialization order and confirm shared scope injection timing. Trace logs reveal whether the host or remote loads first, preventing race conditions during dependency resolution.

Decision Matrix: When to Use Which

The choice between Webpack and Vite federation depends on project scale, legacy constraints, and team expertise. The following rubric isolates technical trade-offs for build engineers and framework maintainers.

Criteria Webpack 5 Module Federation Vite Module Federation
Architecture Static runtime registry (__webpack_require__) Native ESM + Dev-server proxy / Rollup prod
Shared Scope Eager/Lazy hoisting with singleton/requiredVersion HTTP fetch + explicit version negotiation
Dev Experience Slower cold starts, full rebuilds on config changes Fast HMR for app code; federation needs build + preview
Production Single optimized bundle, mature runtime sharing Discrete ESM chunks, requires strict CORS/base alignment
Tree-Shaking Conservative, preserves federated contracts Aggressive, requires explicit sideEffects declarations
Best For Legacy CJS-heavy monoliths, complex runtime sharing Greenfield ESM-first apps, prioritizing dev speed

Implementation Recommendation: Deploy Webpack for legacy CJS-heavy monoliths requiring mature runtime sharing, deterministic chunk boundaries, and complex peer dependency resolution. Adopt Vite for greenfield ESM-first micro-frontends prioritizing developer velocity, native browser compatibility, and reduced build overhead. Before initiating any migration, audit existing peerDependencies, validate sideEffects declarations across the workspace, and establish a strict version negotiation contract across all federated boundaries. Run npx depcheck to identify orphaned shared modules, then incrementally migrate remotes while maintaining a unified CI validation pipeline that asserts remoteEntry.js integrity and shared scope consistency.